1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26<!DOCTYPE html> 27<html> 28<meta name="viewport" content="width=device-width, initial-scale=1" /> 29<link rel="stylesheet" href="style0926.css"> 30<link rel="stylesheet" href="style-header0926.css"> 31 32<head>
33<script src="scripts/RecordRTC_2023-04-19.js"></script>
vendor: 1 bytes, line 33
33
34<script src='scripts/jquery-3.5.1.min.js'></script>
vendor: 1 bytes, line 34
34
35<script src='scripts/jqcookie.js'></script>
vendor: 1 bytes, line 35
35
36<script src="scripts/basicScripts0926.js"></script>
vendor: 1 bytes, line 36
36
37<script src="scripts/talkScriptsTalk0926.js"></script>
37 38 39 40 41 <!-- Google Tag Manager --> 42
42<script>
vendor: 351 bytes, lines 42-46
42(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start': 43 new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0], 44 j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src= 45 'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f); 46 })(window,document,'script','dataLayer','
46GTM-TQF9Z4N
vendor: 3 bytes, line 46
46');
46</script>
46 47 <!-- End Google Tag Manager --> 48 49 50 51<title>Tabbi Talk · English Speaking Practice with AI Feedback</title> 52 <link rel="icon" href="images/favicon-32x32.png" /> 53 <meta name="viewport" content="width=device-width, initial-scale=1" /> 54 55 56 <meta property="og:site_name" content="Tabbi AI" /> 57 <meta property="og:type" content="website" /> 58 <meta property="og:title" content="Tabbi Talk · English Speaking Coach" /> 59 <meta property="og:description" content="Speak for up to 2 minutes. Get your words corrected, feedback on grammar and tone, and a native version to listen to." /> 60 <meta property="og:url" content="https://tabbi.ai/talk" /> 61 <meta property="og:locale" content="en_US" /> 62 <meta property="og:image" content="https://tabbi.ai/images/og-tabbi-en.png" /> 63 <meta property="og:image:width" content="1200" /> 64 <meta property="og:image:height" content="630" /> 65 <meta property="og:image:alt" content="Tabbi AI: speak and write English with confidence" /> 66 <meta name="twitter:card" content="summary_large_image" /> 67 <meta name="twitter:site" content="@tabbiai" /> 68 <meta http-equiv="content-type" content="text/html; charset=utf-8" /> 69 70<meta name="description" content="Speak for up to 2 minutes. Get your words corrected, feedback on grammar and tone, and a native version to listen to."/> 71<meta name="keywords" content="English Speaking Practice, Language Pronunciation Aid, Real-time Speech Correction, Non-native English Support, Interactive Language Learning"/> 72</head> 73 74<body> 75<div class='area-audio invisible'> 76<audio id='voiceGenPlayer' controls><source id='audioSource' type="audio/mpeg">Your browser does not support the audio element.</audio> 77<audio id="playbackPlayer" controls></audio> 78</div> 79 80 81<!-- Google Tag Manager (noscript) -->
vendor: 68 bytes, lines 81-82
81 82<noscript><iframe src="https://www.googletagmanager.com/ns.html?id=
82GTM-TQF9Z4N
vendor: 83 bytes, lines 82-83
82" 83height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
84<!-- End Google Tag Manager (noscript) --> 85 86 87 88<header> 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109
110<script> 111let userIsMember=false; 112let userIsLegacy=false; 113let isTabbiDomain=true; 114let maxCharacters=1000; 115let correctionCount=0; 116let useCustomerIO_thisRequest=false; 117let isNewUser=false; 118let isNewGuest=true; 119/* Welcome cards (2026-09-27, April: existing users must not suddenly see them): a browser's very first page view marks it as new 120 since launch; the cards (WEB-INF/aiccPopups.inc) only ever open in a marked browser. Any page, so it also works when the first page is Home. */ 121try {if (isNewGuest && !localStorage.getItem('tabbiWelcome1')) {localStorage.setItem('tabbiWelcome1','{}');}} catch (e) {} 122let FF_transcriptionPauseDetect=1400; 123let FF_defaultAudioPlaybackRate=1.0; 124let USER_maxQuizViewsPerDay=2; 125let USER_maxCorrectionCount=5; 126let USER_memberChatsRemainingToday=5; 127let FF_quizzesOnDeckToShow=5; 128 129let FF_EchoInputTextPromptLabel="echo_input_text"; 130let FF_ActualReadModeTextPromptLabel="cleanup_pasted_content"; 131let FF_TargetLanguagePromptLabel="target_language_rephrase_edit"; 132let quizGenerationNeeded=false; 133let quizViewCountToday=0; 134let quizzesAvailable=0; 135let showLanguageSurvey=true; 136let thisPageTargetValue="talk"; 137let inputType="TALK"; 138let label2Icon={"original":"🌱","test-quiz-generator-feb21a":"","test-quiz-generator-march18b":"","talk_rephrase_edit_corrected":"âï¸","write_quiz_correction-recall":"ð","talk_rephrase_edit_eloquent":"ð","talk_rephrase_coach_feedback":"ð¨âð«","talk_rephrase_edit_casual":"ð","talk_rephrase_edit_interview-response-STAR":"ð£ï¸","talk_interview_coach_STAR-feedback":"ð","multi_summarize_edit_summary":"â","multi_rephrase_edit_persuade":"ð¤"}; 139let ccErrorMess="To increase your text limit to 5000 characters, join Tabbi Plus!"; 140 141let fullAskTabbiOn=false; 142</script>
142 143 144<div> 145 146 147 148 149<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/orioncactus/[email protected]/dist/web/variable/pretendardvariable-dynamic-subset.min.css"> 150 151<style> 152.popup-message-button-left, .popup-message-button-right {border-radius:8px;} 153#langDialog .popup-message-button-left, #langDialog .popup-message-button-right {border-radius:8px;} 154.button-popup, .button-join-overlay {border-radius:8px;} 155/* Plans: was 8px in a slightly different black (#212427) */ 156.plan-button, .plan-button-gray {border-radius:8px;} 157.plan-button {background-color:#211d1e;} 158.plan-button:hover {background-color:#2e292a;} 159/* after payment: "Start using Tabbi" was 3px */ 160.success-button {border-radius:8px; background-color:#211d1e;} 161.success-button:hover {background-color:#2e292a;} 162/* page surfaces: 8px (they were square) */ 163.oneResult {border-radius:8px; overflow:hidden;} 164.button-prompt-chooser {border-radius:8px;} 165.dailyPageProgressHeader {border-radius:8px;} 166.faq-container {border-radius:8px; overflow:hidden;} 167.section-type-E-inner .info-1, .section-type-F-inner .info-1, .section-typeF-inner .info-1 {border-radius:8px;} 168.plan {border-radius:8px;} /* Plans cards (style-infopage<VER>.css had 10px) */ 169/* floating: 12px (they were 18px) */ 170.popup-message {border-radius:12px;} 171#langDialog {border-radius:12px;} 172.area-alert {border-radius:12px;} 173/* Join / Plus buttons: the versioned stylesheets draw a red ring 2px outside them on any :focus, so a mouse click left what looked like a 174 double line (April). Keep the ring for keyboard users only. */ 175.button-join-overlay:focus:not(:focus-visible), .button-popup:focus:not(:focus-visible) {outline:none;} 176</style> 177 178 179<style> 180/* Width (2026-09-28, April: "cap Write, Talk and Read at 1,100px"). They had no maximum width: on a 1,680px screen the text box and the 181 answers ran ~1,650px wide (lines too long to read, buttons stranded in the middle). History already stops at 1,100px. Phones and 182 screens under ~1,130px are unchanged. */ 183.main-box.write-page, .main-box.talk-page, .main-box.read-page {max-width:1100px; margin-left:auto; margin-right:auto;} 184/* The reset/send bar is fixed to the bottom of the window at 100% width with left:auto, so it started at the centred column's left edge 185 and ran past the right edge: its buttons sat right of centre (April: "buttons are in wrong place"). Pin it to the window's left. */ 186.main-box.write-page .area-buttons-talk, .main-box.talk-page .area-buttons-talk, .main-box.read-page .area-buttons-talk {left:0;} 187/* Read has no AI Style bar, so its box sat 8px under the header while Write/Talk start 16px down (April: "the box is too close to 188 header"). Same 16px there: the whole Read area (box, and the answer that replaces it) moves down 8px. */ 189.main-box.read-page .input-mode-area.read-mode {margin-top:8px;} 190/* Desktop/tablet: a guide line in the AI Style bar's slot and look (April: "add the border around the guide text just like Write and Talk") (same 16px top, 44px tall, same bottom gap), so the box lines up with 191 Write/Talk (April: "read still feel too close to nav"). Phones: hidden, the 8px above keeps 16px under the header. */ 192.read-intro {display:none;} 193@media (min-width:751px) { 194 .main-box.read-page .read-intro {display:flex; align-items:center; justify-content:center; text-align:center; box-sizing:border-box; height:44px; margin:16px 16px .2rem; padding:0 12px; 195 background:#f5f7fa; border:1px solid #d3d3d3; border-radius:8px; font-size:16px; line-height:1.3; color:#475569;} /* the AI Style bar's look, so Read matches Write/Talk (April) */ 196 .main-box.read-page .input-mode-area.read-mode {margin-top:0;} 197} 198/* Tabs above the answer (2026-09-28, April: "use the old design but make the color for width fill out to the side"): the original tabs 199 (white strip right under the header, grey block behind the selected tab) stay as they were; since the column is centred now, the 200 strip's white is extended to both window edges (box-shadow + clip-path, no layout change), so it still reads as a band under the 201 header. The tabs themselves stay in the column, lined up with the card as before. */ 202.main-box.write-page .js-tabtopbar-talk, .main-box.talk-page .js-tabtopbar-talk, .main-box.read-page .js-tabtopbar-talk { 203 box-shadow:0 0 0 100vmax #fff; clip-path:inset(0 -100vmax);} 204/* the first tab starts where the box starts (April: "the button should start where the box start not before"): the strip begins at the 205 column's edge, the card is inset 16px (8px on phones), so the first tab moves in by the same amount */ 206.main-box.write-page .js-tabtopbar-talk .js-tabtop:first-child, .main-box.talk-page .js-tabtopbar-talk .js-tabtop:first-child, .main-box.read-page .js-tabtopbar-talk .js-tabtop:first-child {margin-left:16px;} 207@media only screen and (max-width:750px) { 208 .main-box.write-page .js-tabtopbar-talk .js-tabtop:first-child, .main-box.talk-page .js-tabtopbar-talk .js-tabtop:first-child, .main-box.read-page .js-tabtopbar-talk .js-tabtop:first-child {margin-left:8px;} 209} 210/* No jump on submit (2026-09-29, April: "unlike Read, on Talk and Write, when I submit, the screen jumps up a bit"): sending swaps the 211 AI Style bar for the Original/Corrected/Edited tabs, and the tabs strip w
211as 17px shorter, so the box and then the answer moved up. 212 The strip now takes the bar's height, so the box and the answer stay where they were. Read has neither. 213 Tabs at the strip's bottom (April: "B + remove white empty space under the red line", then "move the whole tabs a bit up so that we 214 will have some space just like the current production. (The cool gray)"): the tabs were centred in the strip (style.css 215 align-self:center), which left white under the selected tab's red line. Now they sit at the strip's bottom, so the red line ends the 216 white strip and the page's cool grey shows between the tabs and the box (9px, production has 8px); the box still doesn't move. */ 217.main-box.write-page .js-tabtopbar-talk, .main-box.talk-page .js-tabtopbar-talk {min-height:62px; align-items:flex-end;} 218.main-box.write-page .js-tabtopbar-talk .js-tabtop, .main-box.talk-page .js-tabtopbar-talk .js-tabtop {align-self:flex-end;} 219@media only screen and (max-width:750px) { 220 .main-box.write-page .js-tabtopbar-talk, .main-box.talk-page .js-tabtopbar-talk {min-height:63px;} 221} 222</style>
223<script> 224/* While the answer loads, select the tab it will open on (2026-09-29, April: "when I submit, it goes to original tab then flash to 225 corrected. It should directly go to corrected."). Sending showed the tabs with Original selected over the faded text, then jumped to 226 Corrected when the answer came. The answer opens on Corrected, or on the first other tab when a style has no Corrected (e.g. 227 translation hides it), so that tab is selected from the start. Write and Talk only; the answer's own tab switch is unchanged. */ 228(function(){ 229 function pickTab(){ 230 const box=document.querySelector('.main-box.write-page, .main-box.talk-page'); if (!box) {return;} 231 const vis=function(e){return e && e.getBoundingClientRect().width>0;}; 232 const tabs=[].slice.call(box.querySelectorAll('.js-tabtopbar-talk .js-tabtop')); if (!tabs.length || !vis(tabs[0].parentNode)) {return;} 233 const target=[box.querySelector('.js-tabtopbar-talk .corrected-tab')].concat(tabs.filter(function(t){return !t.classList.contains('original-tab');})).filter(vis)[0]; 234 if (!target) {return;}
235 tabs.forEach(function(t){t.classList.toggle('js-selected', t===target);}); 236 } 237 document.addEventListener('DOMContentLoaded', function(){ 238 if (!document.querySelector('.main-box.write-page, .main-box.talk-page') || typeof window.showLoadingAnimation!=='function') {return;} 239 const original=window.showLoadingAnimation; 240 window.showLoadingAnimation=function(){ const r=original.apply(this, arguments); setTimeout(pickTab, 0); return r; }; 241 }); 242})(); 243</script>
243 244 245<style> 246/* Site typeface (2026-09-25): one family everywhere. The versioned stylesheets still declare the system stack on body 247 (style/style-infopage) and Verdana on .js-tab-main; this block comes later in the cascade, so it wins at equal 248 specificity without a version bump. Form controls are listed because they never inherit the body font (the Daily 249 start card is a <button>, so it rendered in Arial). Korean pages already set the same stack in langTypography.inc. */ 250body, button, input, select, textarea, .js-tab-main {font-family:"Pretendard Variable", Pretendard, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Apple SD Gothic Neo", "Noto Sans KR", sans-serif;} 251/* Header side room (2026-09-26, April): the logo sat 16px from the left while the menu had 28px on the right. Equal 24px on both 252 sides on desktop: halfway between the 16px content edge of Write/Talk/Read and the old menu inset, so the header reads level 253 with the app pages without hugging the corner. Phones and tablets (the hamburger layout under 851px) keep the stylesheet's spacing. */ 254@media (min-width: 851px) { 255 .navbar {padding-left:24px; padding-right:24px;} 256 .header-logo {margin-left:0;} 257 .account-info-bar .float-right.float-right {padding-right:24px;} /* doubled class: outranks the 20px rule further down */ 258} 259/* Menu, variation 1 (2026-09-25): ink text in one typeface, red only on the active page's underline, rounded ink CTA. Logo and strip unchanged. */ 260.nav-menu span.nav-link, .account-info-bar {font-family:"Pretendard Variable", Pretendard, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Apple SD Gothic Neo", "Noto Sans KR", sans-serif;} 261.nav-menu span.nav-link {position:relative; color:#3d3a3b; font-weight:500; font-size:15px; letter-spacing:-.005em; padding:9px 13px; border-radius:9px;} 262.nav-menu span.nav-link:hover {color:#211d1e; background:#f3f1f2;} 263.nav-menu span.nav-link.js-active {color:#211d1e; font-weight:650;} 264.nav-menu span.nav-link.js-active::after {content:""; position:absolute; left:13px; right:13px; bottom:4px; height:2px; border-radius:2px; background:#bf3143;} 265.nav-menu span.nav-link.tryforfree {color:#fff; background:#211d1e; font-weight:650; padding:10px 16px; border-radius:8px; /* 2026-09-26: 10px like every other button; was the site's only pill */ box-shadow:0 8px 20px -10px rgba(33,29,30,.6);} 266.nav-menu span.nav-link.tryforfree:hover {color:#fff; background:#2e292a;} 267.nav-menu span.nav-link.tryforfree::after {display:none;} 268/* Header refresh A (2026-09-27, April picked option A of three mock-ups). Quieter grey tabs, the current page ink at 600 (was a jump 269 to 650); account links set apart by a thin divider (Log in / History); Join as the ink button like Try for Free; the chats strip 270 without its own divider line and in a smaller, muted type; the logo without the "Unlock your potential" tagline. */ 271.nav-menu span.nav-link {color:#5d6167; letter-spacing:-.01em;} 272.nav-menu span.nav-link:hover {color:#211d1e; background:#f4f4f2;} 273.nav-menu span.nav-link.js-active {color:#211d1e; font-weight:600;} 274.nav-menu span.nav-link.tryforfree, .nav-menu span.nav-link.tryforfree:hover {color:#fff;} 275.account-info-bar, .account-info-bar .float-right {background:#f5f7fa;} /* cool grey utility strip + hairline, same grey as the app background (April 2026-09-27; without any divider the strip and the menu read as one block). Warm #f7f6f4 with #efeef0 was tried; April: "cool grey for now, revisit warm later". */ 276.account-info-bar {border-bottom:1px solid #e8eaee;} 277.account-info-bar .float-right {font-size:13px; color:#736769;} 278@media only screen and (min-width: 851px) { 279 .nav-menu .nav-item.nav-sep {margin-left:12px; padding-left:12px; border-left:1px solid #e6e6e8;} 280 .nav-menu .nav-item.nav-join {margin-left:4px;} 281} 282.lang-switch-mobile a, .lang-switch a {font-weight:500; display:inline-block; text-align:center;} 283/* The selected language is bold, which is wider; each link reserves its bold width with a hidden zero-height copy placed BEFORE the text (after it, the link's baseline would drop and the separators sink) so nothing shifts (April, 2026-09-25). */ 284.lang-switch-mobile a::before, .lang-switch a::before {content:attr(data-t); display:block; height:0; overflow:hidden; visibility:hidden; font-weight:700;} 285/* Strip inset (2026-09-25, April: "a bit of space on the right of Español"): the text sat 3px from the window edge; 20px matches the menu's right edge. */ 286.account-info-bar .float-right {padding-right:20px;} 287@media only screen and (max-width: 750px) { .account-info-bar .float-right {padding-right:16px;} } 288/* Language switch (2026-09-25): in the account strip on wide screens; below the 850px menu breakpoint the strip has no room, so it becomes a stacked block at the end of the hamburger menu. */ 289@media only screen and (max-width: 850px) { 290 .account-info-bar .lang-switch {display:none;} 291 /* tighter phone menu (2026-09-25): the desktop padding plus per-row margins made the list twice as tall as it needs to be */ 292 .nav-menu {padding-top:12px; padding-bottom:12px;} /* 2026-09-27 (April): more room above the first item and below the last (was .4rem) */ 293 .nav-menu {border-radius:12px; border:1px solid #e6e6e8; margin-top:6px; box-shadow:0 22px 52px -20px rgba(0,0,0,.35), 0 5px 14px -8px rgba(0,0,0,.18);} /* 2026-09-28: the phone menu floats, so it follows the pop-up shape (12px, soft shadow), with a small gap under the header */ 294 .nav-menu .nav-item {margin:0 .5rem;} 295 .nav-menu span.nav-link {padding:7px 12px; font-size:15px;} 296 .nav-menu span.nav-link.js-active::after {bottom:3px;} 297 .nav-menu span.nav-link.tryforfree {margin:4px 0 2px; padding:9px 16px;} 298 .nav-menu span.nav-link.lang-switch-mobile {flex-direction:column; align-items:center; gap:6px; white-space:normal; padding:8px 12px 10px; border-top:1px solid #eee; margin-top:4px; border-radius:0; font-size:14px;} 299} 300@media only screen and (min-width: 851px) { .nav-item.lang-item {display:none;} } 301/* Daily's red dot (2026-09-25): hug the word like a badge â the menu's 6px flex gap is enough, no extra margin; lifted a little. */ 302.nav-menu span.nav-link .daily-noti-dot {margin-left:-2px; position:relative; top:-4px;} 303</style>
304<script> 305/* Phone menu (2026-09-27, April): open it just below the menu button. It was pinned at 55px from the top, inside a header that is 306 ~96px tall, so it covered the close X. Measured on each open because the header's height varies by page and account. */ 307document.addEventListener('click', function(e) { 308 var h=e.target && e.target.closest ? e.target.closest('.hamburger') : null; if (!h) return; 309 var m=document.querySelector('.nav-menu'); if (!m) return; 310 if (window.innerWidth<=850) {m.style.top=Math.round(h.getBoundingClientRect().bottom+10)+'px';} else {m.style.top='';} 311}, true); 312</script>
312 313 314 315 <div class='account-info-bar'><span class='float-right'> 316 317 318 319 320 <span class='topRemMessage'>You have</span> 321 <span class='chatsRemaining maxwidth1'>5</span> <span class='maxwidth2'> free chats remaining </span> | 322 <a class='subsMessage' href="#" onclick='showX($(".area-agreement"));'>Sign up</a> for more 323 324 325 326 </span></div> 327 328 329<div class="horizontal-line"> 330<nav class="navbar"> 331<a class='header-logo' target="_self" href="./home" aria-current="page"><img src="images/tabbi-logo-v2.png" width="185" alt="Tabbi AI"></a> 332<ul class="nav-menu"> 333 334 335 336 337 <li class="nav-item"> 338 <a href="#" class="nav-link"> 339 <span class='dailyMenuItem nav-link new-content' onclick='catoutThenLink("MENU ITEM: Guest/Daily","daily");'> 340 Daily 341 <span class="daily-noti-dot"></span></span></a> 342 343
343<script>try{if(localStorage.getItem('dailyGuestDoneId')=='475'){var gd=document.querySelector('.dailyMenuItem .daily-noti-dot');if(gd){gd.remove();}}}catch(e){}</script>
343 344 </li> 345 <li class="nav-item"> 346 <a href="#" class="nav-link"><span class='chatMenuItem nav-link' onclick='catoutThenLink("MENU ITEM: Guest/Write","write");'>Write</span></a> 347 </li> 348 <li class="nav-item"> 349 <a href="#" class="nav-link"><span class='talkMenuItem nav-link js-active' onclick='catoutThenLink("MENU ITEM: Guest/Talk","talk");'>Talk</span></a> 350 </li> 351 352 <li class="nav-item"> 353 <a href="#" class="nav-link"><span class='readMenuItem nav-link' onclick='catoutThenLink("MENU ITEM: Guest/Read","read");'>Read</span></a> 354 </li> 355 356 357 358 <li class="nav-item nav-sep"> 359 <a href="#" class="nav-link"><span class='loginMenuItem nav-link' onclick='catoutThenLink("MENU ITEM: Guest/Login","https://auth.tabbi.ai/login?client_id=34t7p7b7r0sc2799h1rcvjksse&response_type=code&scope=email+openid+phone&redirect_uri=https%3A%2F%2Ftabbi.ai%2Fauth.jsp%3Fpage=talk");'>Log in</span></a> 360 </li> 361 <li class="nav-item nav-join"> 362 <a href="#" class="nav-link"><span class="joinMenuItem tryforfree nav-link" onclick="catout('MENU ITEM: Guest/Join'); showX($('.area-agreement'));">Join</span></a> 363 </li> 364 365 366 367</ul> 368 <div class="hamburger"> 369 <span class="bar"></span> 370 <span class="bar"></span> 371 <span class="bar"></span> 372 </div> 373 </nav> 374 375 </div> 376</div> 377 378</header> 379<style id="popup-phone-fit"> 380/* Phones (2026-09-24): style0911.css sizes .popup-message as content + 48px padding, so the box ran past the screen. Border-box here keeps the 14px side margins the stylesheet intended; desktop untouched. */ 381@media only screen and (max-width: 430px) { 382 .popup-message {box-sizing:border-box; width:min(392px, calc(100vw - 28px));} 383} 384</style> 385 386 387
388<script src="scripts/popupsScripts0926.js"></script>
388 389<div class='popupOverlay invisible'></div> 390
391<script> 392$(function(){setupSnippetFunctionality();}); 393</script>
393 394 395 396 397<!-- history unlock (Tabbi Plus upsell for locked history rows) start --> 398 <div class="popup-message history-unlock-dialog invisible"> 399 <span class="popup-close-button" onclick="hideX($('.popup-message'));"><img src="images/close-x-trans.png"></span> 400 <span class="popup-message-title">Unlock your history</span> 401 <span class="popup-message-text">Free accounts keep chats for 24 hours. Tabbi Plus keeps everything — this one, and every chat you've ever written.</span> 402 <div class="popup-message-button-area"> 403 <button type="button" class="popup-message-button-left pmb-ghost" onclick="hideX($('.popup-message'));">Not now</button> 404 <button type="button" class="popup-message-button-right pmb-ink" onclick='catoutThenLink("UPSELL: history unlock","plans");'>Start 14-day free trial</button> 405 </div> 406 </div> 407<!-- history unlock end --> 408 409<!-- delete all chats (History page) start --> 410 <div class="popup-message delete-all-dialog invisible"> 411 <span class="popup-close-button" onclick="hideX($('.popup-message'));"><img src="images/close-x-trans.png"></span> 412 <span class="popup-message-title">Delete all chats?</span> 413 <span class="popup-message-text">This removes <span class="delete-all-count">every chat</span> from your History. It can't be undone.</span> 414 <div class="popup-message-button-area"> 415 <button type="button" class="popup-message-button-left pmb-ghost" onclick="hideX($('.popup-message'));">Cancel</button> 416 <button type="button" class="popup-message-button-right pmb-ink" onclick="deleteAllChats();">Delete all</button> 417 </div> 418 </div> 419<!-- delete all chats end --> 420 421<div class='movablePopup snippetOptions toolsTarget js-bs-top invisible prevent-select' starOn=false> 422 <div class='tool-grabber'><center><img class="movable" src="images/movable.png"></center></div> 423 <div class='tool-box'> 424 <div class='clickable'><span id='toolbox-star' class='snip-tool-button button-favorite' onclick='toggleStarGoldSnippet(this);'><img class='icon-multi image-one' src='images/highlight-black.png'><img class='icon-multi image-two invisible' src='images/highlight-yellow.png'></span></div> 425 <div class='clickable'><span id='toolbox-read' class='snip-tool-button button-read-aloud' onclick='setDiffState(false,nowTabContent); toggleAudio(this,$(".snip").text());'><img class='icon-multi image-one' src='images/read.png'><img class='icon-multi image-two invisible' src='images/read-selected.png'></span></div> 426 <div class='clickable copy-position'><span class='guide-copied'>Copied</span><span id='toolbox-copy' class='snip-tool-button button-copy' onclick='clipboardSimple(this,"snippetOptions","snip");'><img class='icon-multi image-one' src='images/copy.png' ><img class='icon-multi image-two invisible' src='images/copy-selected.png' ></span></div> 427 <div class='clickable'><span id='toolbox-meaning' class='snip-tool-button button-meaning aiButton' onclick='fetchSnippetResults(this,"meaning");'><img class='icon-multi image-one' src='images/meaning-black.png'><img class='icon-multi image-two invisible' src='images/meaning-red.png'></span></div> 428 <div class='clickable'><span id='toolbox-rephrase' class='snip-tool-button button-rephrase aiButton' onclick='fetchSnippetResults(this,"rephrased");'><img class='icon-multi image-one' src='images/rephrase-black.png'><img class='icon-multi image-two invisible' src='images/rephrase-red.png'></span></div> 429 430 </div> 431 432 <div class='snipTitle invisible'><span class='snip'></span></div> 433 <div class='snipContext invisible'></div> 434 <div class='snipAIArea popupTextToCopy'></div> 435 <center><img class='spinner invisible' src='images/loading-gif.gif' loading='lazy'></center> 436</div> 437 438<style id="read-popup"> 439/* Read highlight pop-up (2026-09-28, April). The last pop-up in the old grey 5px style. Floating layers are rounded with a shadow 440 (quiz, Ask Tabbi, dialogs: 12px; page surfaces 8px), so it becomes the same family as Ask Tabbi: a white 12px card whose icon row 441 sits on the light warm grey strip, answers (Meaning / Rephrased / Translation) on white under a small grey label, Copy as the 442 toolbar's copy icon whose tip turns into "Copied". Scoped to .snippetOptions; the pop-up is still placed with left/top (no 443 transform), which tabbiTips.inc relies on for its fixed-position tips. */ 444.snippetOptions {width:16rem; padding:0; border:1px solid #e7e3de; border-radius:12px; background:#fff; overflow-x:hidden; overflow-y:auto; 445 box-shadow:0 22px 52px -20px rgba(0,0,0,.35), 0 5px 14px -8px rgba(0,0,0,.18);} 446.snippetOptions .tool-box {background:#f6f5f3; padding:6px 8px; gap:2px; justify-content:space-between;} 447.snippetOptions .snipAIArea:not(:empty) {border-top:1px solid #ece8e3;} 448.snippetOptions .oneSnipAI {margin:0; padding:12px 14px 10px; border:0; border-radius:0; background:#fff; font-size:15px; line-height:1.55; color:#2f2b2c; display:flex; flex-wrap:wrap; align-items:center;} 449.snippetOptions .oneSnipAI.invisible {display:none;} 450.snippetOptions .oneSnipAI .copyTarget {flex:1 1 100%;} 451.snippetOptions .oneSnipAI .snipAITitle {display:block; margin:0 0 4px; font-size:12.5px; font-weight:700; letter-spacing:.06em; text-transform:uppercase; color:#736769;} 452.snippetOptions .oneSnipAI .guide-copied {display:none;} 453.snippetOptions .oneSnipAI .popupCopyButton {position:relative; display:inline-flex; width:32px; height:32px;
453 margin:6px -8px -6px auto; padding:0; border:0; border-radius:8px; 454 background:transparent url('images/copy.png') center / 20px 20px no-repeat; font-size:0; color:transparent; opacity:.75; cursor:pointer; bottom:auto; right:auto;} 455.snippetOptions .oneSnipAI .popupCopyButton:hover {background-color:#f4f2ef; opacity:1;} 456.snippetOptions .spinner {margin:12px 0;} 457/* "Copied": the same one-pill behaviour as Ask Tabbi (script toggles .visible on .guide-copied for 2 s) */ 458@media (hover:hover) { 459 .snippetOptions .oneSnipAI .popupCopyButton::after {content:'Copy'; position:absolute; right:0; bottom:calc(100% + 6px); background:#211d1e; color:#fff; 460 font-size:12px; line-height:1; font-weight:500; padding:6px 9px; border-radius:6px; white-space:nowrap; pointer-events:none; opacity:0; transition:opacity .12s ease .6s;} 461 .snippetOptions .oneSnipAI .popupCopyButton:hover::after {opacity:1;} 462} 463.snippetOptions .oneSnipAI .guide-copied.visible + .popupCopyButton {opacity:1;} 464.snippetOptions .oneSnipAI .guide-copied.visible + .popupCopyButton::after {content:'Copied'; position:absolute; right:0; bottom:calc(100% + 6px); background:#211d1e; color:#fff; 465 font-size:12px; line-height:1; font-weight:500; padding:6px 9px; border-radius:6px; white-space:nowrap; pointer-events:none; opacity:1; transition:none;} 466</style>
467<script> 468// The script writes "<span class='snipAITitle'>meaning</span>: text"; with the title as its own label line, drop the ": " it leaves. 469$(function(){ 470 var area=document.querySelector('.snippetOptions .snipAIArea'); if (!area || !window.MutationObserver) return; 471 new MutationObserver(function(){ 472 area.querySelectorAll('.oneSnipAI .snipAITitle').forEach(function(t){ 473 var n=t.nextSibling; if (n && n.nodeType===3 && /^\s*:\s*/.test(n.nodeValue)) {n.nodeValue=n.nodeValue.replace(/^\s*:\s*/,'');} 474 }); 475 }).observe(area, {childList:true}); 476}); 477</script>
vendor: 1 bytes, line 477
477
478<script> 479$(function(){setupQuizFunctionality();}); 480</script>
480 481 482<div class='quiz-onoff cat1 invisible'><span class='quiz-button' onclick='toggleQuiz();'><img src='images/quiz-cat1.gif' width="70" height="70" loading="lazy"/></span></div> 483<div class='quiz-onoff cat2 invisible'><span class='quiz-button' onclick='toggleQuiz();'><img src='images/quiz-cat2.gif' width="70" height="70" loading="lazy"/></span></div> 484 485 486<style id="quiz-card"> 487.quizPopup {width:min(352px, calc(100vw - 24px)); max-height:min(34rem, calc(100dvh - 24px)); padding:20px 18px 16px; box-sizing:border-box; 488 background:#f6f5f3; border:1px solid #e7e3de; border-radius:12px; color:#211d1e; line-height:1.45; 489 box-shadow:0 22px 52px -20px rgba(0,0,0,.35), 0 5px 14px -8px rgba(0,0,0,.18);} 490.quizPopup.glowingDrag {background:#f1efeb; border:1px solid #d8d8dc;} 491.quizPopup .popup-close-button {top:12px; right:12px; width:32px; height:32px; border-radius:8px; display:flex; align-items:center; justify-content:center; opacity:.6;} 492.quizPopup .popup-close-button:hover {opacity:1; background:#ebe8e3;} 493.quizPopup .popup-close-button img {width:16px;} 494.quizPopup .quizAIArea {padding:0; font-size:14px; color:#736769;} /* "Loading..." */ 495.quizPopup img.spinner {width:30px; height:30px;} /* style0926.css sets every img in the quiz to 25px wide, which squeezed the round loader (2026-09-28) */ 496.quizPopup .oneQuizAI {color:#211d1e;} 497.quizPopup .oneQuizAI .quizTitle {font-size:1.2rem; font-weight:700; letter-spacing:-.015em; line-height:1.25; padding:2px 36px 12px 2px;} 498/* 2026-09-28 (April: "the quiz card is too white"): the card opens over Write's white text box, so it is a light warm grey (#f6f5f3; April 499 compared it with the cool #f5f7fa and chose warm) with white answer rows and a white "Why?" panel, and a small tag says what popped up. */ 500.quizPopup .oneQuizAI .quizTitle::before {content:'Quick quiz'; display:block; width:fit-content; margin:0 0 10px; padding:4px 10px; border-radius:999px; 501 background:#fbe9ec; color:#bf3143; font-size:12px; font-weight:700; letter-spacing:.02em; line-height:1.2;} 502/* the two answers: tappable cards with a round marker */ 503.quizPopup .oneQuizAI .quiz-choice-all {display:flex; align-items:center; gap:12px; margin:0 0 8px; padding:12px 14px; padding-right:12px; 504 background:#fff; border:1.5px solid #e7e3de; border-radius:8px; transition:border-color .15s, background-color .15s, opacity .15s;} 505.quizPopup.unclicked .oneQuizAI .quiz-choice-all {cursor:pointer;} 506.quizPopup.unclicked .oneQuizAI .quiz-choice-all:hover {border-color:#211d1e;} 507.quizPopup .oneQuizAI .quiz-choice-all .quiz-choice {flex:1 1 auto; margin:0; padding:0; border:0 !important; background:transparent; font-size:15px; line-height:1.4; color:#211d1e;} 508.quizPopup.unclicked .oneQuizAI .quiz-choice-all:hover .quiz-choice {border:0 !important;} 509.quizPopup .oneQuizAI .quiz-choice-icon {flex:0 0 22px; width:22px; height:22px; border-radius:50%; border:1.5px solid #c9c9c5; box-sizing:border-box; 510 background:#fff center / 12px 12px no-repeat;} 511.quizPopup .oneQuizAI .quiz-choice-icon img {display:none !important;} 512.quizPopup.unclicked .oneQuizAI .quiz-choice-all:hover .quiz-choice-icon {border-color:#211d1e;} 513/* answered: the better sentence turns green with a check; a wrong pick turns red with an x; the other fades */ 514.quizPopup:not(.unclicked) .oneQuizAI .quiz-correct {border-color:#488474; background:#eff6f2;} 515.quizPopup:not(.unclicked) .oneQuizAI .quiz-correct .quiz-choice-icon {border:0; background-color:#488474; 516 background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3Cpath d='M2.5 6.2l2.3 2.3 4.7-4.9' fill='none' stroke='white' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");} 517.quizPopup:not(.unclicked) .oneQuizAI .quiz-incorrect.choice-chosen {border-color:#bf3143; background:#fdf1f2;} 518.quizPopup:not(.unclicked) .oneQuizAI .quiz-incorrect.choice-chosen .quiz-choice-icon {border:0; background-color:#bf3143; 519 background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3Cpath d='M3.3 3.3l5.4 5.4M8.7 3.3l-5.4 5.4' fill='none' stroke='white' stroke-width='1.8' stroke-linecap='round'/%3E%3C/svg%3E");} 520.quizPopup:not(.unclicked) .oneQuizAI .quiz-incorrect:not(.choice-chosen) {opacity:.55;} 521/* the verdict (added by the script below) and the reason, in one soft panel */ 522.quizPopup .oneQuizAI .quiz-result-explained {margin:6px 0 0; padding:12px 14px; border-radius:8px; background:#fff; border:1px solid #e7e3de; font-size:14px; line-height:1.5; color:#3d3a3b;} 523.quizPopup .oneQuizAI .quiz-result-explanation {font-weight:700; color:#211d1e;} 524.quizPopup .quiz-verdict {display:block; font-size:15px; font-weight:700; margin:0 0 4px;} 525.quizPopup .quiz-verdict.ok {color:#3a7362;} 526.quizPopup .quiz-verdict.no {color:#bf3143;} 527.quizPopup .quizAITool-box .result-message {display:none !important;} /* shown in the panel instead */ 528/* actions: Review in History as a quiet link, Next Quiz as the ink button */ 529.quizPopup .quizAITool-box {display:flex; align-items:center; gap:12px; margin-top:14px;} 530.quizPopup .quizAITool-box:not(:has(> span:not(.invisible):not(.result-message))) {margin-top:0;} 531.quizPopup .quiz-history-link {margin:0; font-size:14px; text-decoration:none;}
531 /* style0926.css underlines this span too: with the link's own underline it showed two lines */ 532.quizPopup .quiz-history-link a {color:#5d6167; text-decoration:underline; text-decoration-color:#c9c9c5; text-underline-offset:3px;} 533.quizPopup .quiz-history-link a:hover {color:#211d1e; text-decoration-color:#211d1e;} 534.quizPopup .another-quiz-button {margin-left:auto; font-size:14px;} 535.quizPopup .another-quiz-button a {display:inline-flex; align-items:center; height:38px; padding:0 16px; border-radius:8px; background:#211d1e; color:#fff; 536 font-weight:650; text-decoration:none; box-shadow:0 6px 16px -8px rgba(33,29,30,.5);} 537.quizPopup .another-quiz-button a:hover {background:#2e292a;} 538</style>
539<script> 540/* Verdict at the top of the "Why?" panel, right after an answer: "Meow! That's right." / "Hiss... The other one is better." 541 Worked out from the classes the quiz script sets, and rebuilt with each quiz (the panel is replaced), so it never goes stale. */ 542document.addEventListener('click', function(e) { 543 var c=e.target && e.target.closest ? e.target.closest('.quizPopup .quiz-choice-all') : null; if (!c) return; 544 setTimeout(function(){ 545 var box=document.querySelector('.quizPopup .quiz-result-explained'); if (!box || box.querySelector('.quiz-verdict')) return; 546 var picked=document.querySelector('.quizPopup .quiz-choice-all.choice-chosen'); if (!picked) return; 547 var ok=picked.classList.contains('quiz-correct'); var v=document.createElement('span'); 548 v.className='quiz-verdict '+(ok?'ok':'no'); v.textContent=ok ? 'Meow! Thatâs right.' : 'Hiss⦠The other one is better.'; 549 box.insertBefore(v, box.firstChild); 550 }, 0); 551}); 552/* A new quiz starts unanswered (2026-09-27 fix). The quiz script removes .unclicked on an answer but never puts it back for the 553 next quiz, and the card above reads .unclicked to know "not answered yet", so Next Quiz showed the new answer already marked. 554 Whenever the quiz area is refilled ("Loading...", then the new question) and nothing is chosen in it, mark the card unanswered. */ 555(function(){ 556 function init() { 557 var area=document.querySelector('.quizPopup .quizAIArea'); if (!area || !window.MutationObserver) return; 558 new MutationObserver(function(){ 559 if (!area.querySelector('.choice-chosen')) {var p=area.closest('.quizPopup'); if (p) p.classList.add('unclicked');} 560 }).observe(area, {childList:true}); 561 } 562 if (document.readyState==='loading') document.addEventListener('DOMContentLoaded', init); else init(); 563})(); 564</script>
564 565<div class='movablePopup quizPopup toolsTarget js-bs-top invisible prevent-select unclicked' starOn=false> 566 <span class='popup-close-button' onclick='closePopupQuiz();'><img class='mainImg' src="images/close-x-trans.png"/></span> 567 <div class='quizAIArea'></div> 568 <center><img class='spinner invisible' src='images/loading-gif.gif' loading='lazy'></center> 569 <div class='quizAITool-box'> 570 <span class='result-message correct-choice invisible'>Meow!</span> 571 <span class='result-message incorrect-choice invisible'>Hiss...</span> 572 <span class='quiz-history-link invisible'><a href='historyOne.jsp'>Review in History</a></span> 573 <span class='another-quiz-button invisible'><a href='#' onclick='fetchAndShowQuizResults();'>Next Quiz</a></span> 574 </div> 575</div>
576<script> 577// Quiz card position (2026-09-28, April: "on mobile the quiz is not in the center and touches the right side"). 578// Replaces toggleQuiz from scripts/popupsScripts*.js (loaded above, so this later definition wins; no version bump). 579// The old code centred with jQuery .width(), which leaves out the card's padding and border (38px), so the card sat 19px 580// right of centre: on a phone its right edge met the screen edge. Centre on the window with the full outer width instead, 581// and keep at least 12px on each side. Dragging still works: the card is only placed when it opens. 582async function toggleQuiz() { 583 currentMovablePopup=$('.movablePopup.quizPopup'); 584 toggleX(currentMovablePopup); 585 if (!quizShown) { 586 currentMovablePopup.addClass('unclicked'); 587 let vw=document.documentElement.clientWidth, w=$(".quizPopup").outerWidth(); 588 let xpLeft=Math.max(12, Math.round((vw-w)/2)) + window.scrollX; 589 let xpTop=180; 590 $(".quizPopup").css({left: xpLeft, top: xpTop}); 591 quizShown=true; 592 fetchAndShowQuizResults(); 593 } 594} 595</script>
595 596
597<script> 598$(function(){setupAskTabbiFunctionality();}); 599</script>
599 600 601<div id="floating-AT" class='ask invisible' style='position: absolute;'><span><img src="images/asktabbi.png"></span></div> 602<div class='movablePopup askTabbiPopup toolsTarget js-bs-top invisible prevent-select unclicked' starOn=false> 603 <span class='popup-close-button' onclick='closePopupAskTabbi();'><img class='mainImg' src="images/close-x-trans.png"/></span> 604 <div class="askTabbiArea"> 605 606 <div class="askTabbiResults atAvailable"> 607 <div class="askTabbiResults-title"><span class="at-tag">Ask Tabbi</span><span class='atTargetInTitle'></span></div> 608 <div class="askTabbiResults-rest"> 609 <div class="askTabbiResults-loading"><center><img class="spinner" src="images/loading-gif.gif" loading="lazy"></center></div> 610 <div class="askTabbiResults-text popupTextToCopy invisible"></div> 611 </div> 612 </div> 613 614 <div class="askTabbiResults atUnavailable invisible"> 615 616 <div class="askTabbiResults-title"><span class="at-tag">Ask Tabbi</span>2 explanations per chat</div> 617 <div class="askTabbiResults-basic"> 618 <div class="askTabbiResults-basic-text">You've used both explanations for this chat. Start a new chat for 2 more, or get unlimited explanations with <strong>Tabbi Plus</strong>.</div> 619 <a href="https://tabbi.ai/plans" target="_blank" onclick="closePopupAskTabbi();"><button class="askTabbiResults-basic-button">Try Plus free for 14 days</button></a> 620 </div> 621 </div> 622 623 </div> 624</div> 625 626 627<style id="ask-tabbi-card"> 628/* Ask Tabbi (2026-09-28, April: "let's work on ask tabbi feature to match the design"). April compared four designs (A warm card with a 629 pill, B all white, C/C2 warm with the logo, D) and chose D: a white card, so the explanation reads on white, with a light warm grey 630 header strip (#f6f5f3, the quiz card's colour) holding the Ask Tabbi cat logo, "Ask Tabbi" and the word in quotes. The category the 631 answer starts with ("Nuanced improvement") is a small grey label, Copy is the toolbar's copy icon whose tip turns into "Copied". 632 The dotted strip is gone (the whole card still drags). Scoped to .askTabbiPopup so the old style0926.css rules lose. */ 633.askTabbiPopup {width:min(352px, calc(100vw - 24px)); max-height:min(40rem, calc(100dvh - 200px)); padding:0 18px 16px; box-sizing:border-box; 634 background:#fff; border:1px solid #e7e3de; border-radius:12px; color:#211d1e; line-height:1.45; overflow-x:hidden; overflow-y:auto; overscroll-behavior:contain; 635 box-shadow:0 22px 52px -20px rgba(0,0,0,.35), 0 5px 14px -8px rgba(0,0,0,.18);} 636.askTabbiPopup::before {display:none;} 637.askTabbiPopup.glowingDrag {background:#fbfaf9; border:1px solid #d8d8dc;} 638.askTabbiPopup .popup-close-button {top:12px; right:12px; width:32px; height:32px; border-radius:8px; display:flex; align-items:center; justify-content:center; opacity:.6; z-index:1;} 639.askTabbiPopup .popup-close-button:hover {opacity:1; background:#ebe8e3;} 640.askTabbiPopup .popup-close-button img {width:16px;} 641.askTabbiPopup .askTabbiArea {max-height:none; overflow:visible; padding:0;} 642/* header strip: logo + "Ask Tabbi", then the word in quotes (added words green and underlined, removed words red and struck through) */ 643.askTabbiPopup .askTabbiResults-title {margin:0 -18px 14px; padding:18px 54px 14px 20px; background:#f6f5f3; border-bottom:1px solid #ece8e3; 644 font-size:1.35rem; font-weight:700; letter-spacing:-.015em; line-height:1.25;} 645.askTabbiPopup .at-tag {display:flex; align-items:center; gap:7px; margin:0 0 8px; color:#bf3143; font-size:13.5px; font-weight:700; letter-spacing:.01em; line-height:1.2;} 646.askTabbiPopup .at-tag::before {content:''; flex:0 0 22px; width:22px; height:22px; background:url('images/asktabbi.png') center / contain no-repeat;} 647.askTabbiPopup .atTargetInTitle::before {content:'\201C'; color:#211d1e;} 648.askTabbiPopup .atTargetInTitle::after {content:'\201D'; color:#211d1e;} 649.askTabbiPopup .atTargetInTitle span {background:none; border:0; padding:0;} 650.askTabbiPopup .atTargetInTitle .diff-delete {color:#c0606c;} /* the light pink of the answer text is hard to read at title size */ 651.askTabbiPopup .askTabbiResults-loading {margin:1.25rem 0 .5rem;} 652.askTabbiPopup img.spinner {width:30px; height:30px;} /* style0926.css sets every img here to 25px wide, which squeezed the round loader */ 653/* the answer, on the white card;
653 "Copied" + Copy sit at its foot. A long answer scrolls the whole card (it opens 180px down). */ 654.askTabbiPopup .askTabbiResults-text {padding:0 2px; font-size:15px; line-height:1.55; color:#2f2b2c; max-height:none; overflow:visible;} 655.askTabbiPopup .askTabbiResults-text:not(.invisible) {display:flex; flex-wrap:wrap; align-items:center;} 656.askTabbiPopup .askTabbiResults-text .copyTarget {flex:1 1 100%;} 657.askTabbiPopup .askTabbiResults-text p {margin:0 0 .6rem;} 658.askTabbiPopup .askTabbiResults-text p:last-child {margin-bottom:0;} 659.askTabbiPopup .ask-tabbi-title {display:block; margin:0 0 6px; font-size:12.5px; font-weight:700; letter-spacing:.06em; text-transform:uppercase; color:#736769;} 660/* Copy is the answer toolbar's copy icon (images/copy.png), April: "can we use icon for Copy?". Placed in the card's corner, mirroring the close x 661 (April: "more bottom more right"): its centre sits under the x's centre and as far from the bottom as the x is from the top. The script writes the word "Copy" 662 into this span, so the word is hidden (font-size 0) and the icon is its background; the same "Copy" tip as the toolbar on hover. */ 663.askTabbiPopup .askTabbiResults .popupCopyButton {position:relative; display:inline-flex; width:32px; height:32px; margin:8px -21px -8px auto; padding:0; box-sizing:border-box; 664 border:0; border-radius:8px; background:transparent url('images/copy.png') center / 20px 20px no-repeat; font-size:0; color:transparent; opacity:.75; cursor:pointer;} 665.askTabbiPopup .askTabbiResults .popupCopyButton:hover {background-color:#f4f2ef; opacity:1;} 666@media (hover:hover) { 667 .askTabbiPopup .askTabbiResults .popupCopyButton::after {content:'Copy'; position:absolute; right:0; bottom:calc(100% + 6px); background:#211d1e; color:#fff; 668 font-size:12px; line-height:1; font-weight:500; padding:6px 9px; border-radius:6px; white-space:nowrap; pointer-events:none; opacity:0; transition:opacity .12s ease .6s;} 669 .askTabbiPopup .askTabbiResults .popupCopyButton:hover::after {opacity:1;} 670} 671/* "Copied" is not shown as its own grey word: the Copy tip itself turns into "Copied" (April: "so that I don't need to see two"), 672 like the answer toolbar. The script still toggles .visible on this span for 2 s after a copy; the rules below key off it. */ 673.askTabbiPopup .askTabbiResults .guide-copied {display:none;} 674.askTabbiPopup .askTabbiResults .guide-copied.visible + .popupCopyButton {opacity:1;} /* the faded icon would fade the pill too */ 675.askTabbiPopup .askTabbiResults .guide-copied.visible + .popupCopyButton::after {content:'Copied'; position:absolute; right:0; bottom:calc(100% + 6px); 676 background:#211d1e; color:#fff; font-size:12px; line-height:1; font-weight:500; padding:6px 9px; border-radius:6px; white-space:nowrap; pointer-events:none; 677 opacity:1; transition:none;} 678/* limit reached: no header strip, so the card's own top padding comes back here; the site's red button */ 679.askTabbiPopup .askTabbiResults-basic {padding:0 2px;} 680.askTabbiPopup .askTabbiResults-basic-title {font-size:1.2rem; font-weight:700; line-height:1.25; padding-right:36px;} 681.askTabbiPopup .askTabbiResults-basic-text {font-size:15px; line-height:1.5; color:#3d3a3b; margin:8px 0 16px;} 682.askTabbiPopup .askTabbiResults-basic-button {height:42px; padding:0 16px; border:0; border-radius:8px; background:#bf3143; color:#fff; font-size:15px; font-weight:650; 683 box-shadow:0 6px 16px -8px rgba(191,49,67,.7);} 684.askTabbiPopup .askTabbiResults-basic-button:hover {background:#a3283a;} 685</style>
686<script> 687// Ask Tabbi position (2026-09-28): replaces openPopupAskTabbi from scripts/popupsScripts*.js (loaded above; no version bump). 688// Same bug the quiz had: it centred with jQuery .width(), which leaves out padding and border, so the card sat right of centre 689// and could touch the right edge on phones; and the "limit reached" card was never placed at all. Both now use atPlace(). 690function atPlace(p) { 691 let vw=document.documentElement.clientWidth, w=p.outerWidth(); 692 p.css({left: Math.max(12, Math.round((vw-w)/2)) + window.scrollX, top: 180}); 693} 694// The script adds ": " inside the category span ("Nuanced Improvement: "); as a small label it reads better without it. 695$(function(){ 696 var box=document.querySelector('.askTabbiPopup .askTabbiResults-text'); if (!box || !window.MutationObserver) return; 697 new MutationObserver(function(){ 698 var t=box.querySelector('.ask-tabbi-title'); if (t && /:\s*$/.test(t.textContent)) {t.textContent=t.textContent.replace(/:\s*$/,'');} 699 }).observe(box, {childList:true}); 700}); 701async function openPopupAskTabbi(fullContext, insertOrDelete, spanContent) { 702 if (askTabbiInProgress) {return;} 703 currentMovablePopup=$('.movablePopup.askTabbiPopup'); 704 if (!(fullAskTabbiOn || askTabbiCount<MAX_AskTabbi)) { 705 hideX($('.askTabbiResults')); showX($('.atUnavailable')); showX(currentMovablePopup); 706 if (!askTabbiShown) {atPlace(currentMovablePopup); askTabbiShown=true;} 707 return; 708 } 709 710 askTabbiInProgress=true; 711 if (!askTabbiShown) { 712 hideX($('.askTabbiResults')); showX($('.atAvailable')); 713 showX(currentMovablePopup); 714 currentMovablePopup.addClass('unclicked'); 715 atPlace(currentMovablePopup); 716 askTabbiShown=true; 717 } 718 fetchAndShowAskTabbiResults(fullContext, insertOrDelete, spanContent); 719 askTabbiCount+=1; 720} 721</script>
721 722 723 724<div class='movablePopup feedbackPopup js-bs-top invisible' role='dialog' aria-label='Feedback'> 725 <span class='popup-close-button' onclick='closePopupFeedback();'><img class='mainImg' src="images/close-x-trans.png" alt="Close"/></span> 726 <div class='fbp-head'>Feedback</div> 727 <div class='fbp-text'></div> 728</div> 729<style id="feedback-card"> 730.feedbackPopup {position:absolute; z-index:1000; width:min(352px, calc(100vw - 24px)); max-height:min(34rem, calc(100dvh - 32px)); padding:0 18px 16px; box-sizing:border-box; 731 background:#fff; border:1px solid #e7e3de; border-radius:12px; color:#211d1e; line-height:1.45; overflow-x:hidden; overflow-y:auto; overscroll-behavior:contain; 732 box-shadow:0 22px 52px -20px rgba(0,0,0,.35), 0 5px 14px -8px rgba(0,0,0,.18);} 733.feedbackPopup.glowingDrag {background:#fbfaf9; border:1px solid #d8d8dc;} 734.feedbackPopup .popup-close-button {top:12px; right:12px; width:32px; height:32px; border-radius:8px; display:flex; align-items:center; justify-content:center; opacity:.6; z-index:1;} 735.feedbackPopup .popup-close-button:hover {opacity:1; background:#ebe8e3;} 736.feedbackPopup .popup-close-button img {width:16px;} 737.feedbackPopup .fbp-head {margin:0 -18px 14px; padding:18px 54px 14px 20px; background:#f6f5f3; border-bottom:1px solid #ece8e3; font-size:1.2rem; font-weight:700; letter-spacing:-.01em; line-height:1.25; cursor:move;} 738.feedbackPopup .fbp-text {padding:0 2px; font-size:15px; line-height:1.55; color:#2f2b2c;} 739.feedbackPopup .fbp-text p {margin:0 0 .6rem;} .feedbackPopup .fbp-text p:last-child {margin-bottom:0;} 740</style>
741<script> 742function closePopupFeedback() {hideX($('.feedbackPopup')); $('.feedback-button').removeClass('js-active');} 743// Runs before the button's own handler (capture phase) and stops it, so the answer is no longer swapped for the feedback. 744document.addEventListener('click', function(e) { 745 var b=e.target && e.target.closest ? e.target.closest('.oneResult-corrected .feedback-button') : null; if (!b) return; 746 e.stopPropagation(); e.preventDefault(); 747 var pop=document.querySelector('.feedbackPopup'); if (!pop) return; 748 if (!pop.classList.contains('invisible') && pop._from===b) {closePopupFeedback(); return;} // a second click closes it 749 var scope=b.closest('.oneResultCombo') || b.closest('.js-roof-node') || document; 750 var src=scope.querySelector('.oneResult-feedback .contentSpan') || scope.querySelector('.oneResult-feedback .result-content'); 751 pop.querySelector('.fbp-text').innerHTML=src ? src.innerHTML : ''; 752 pop._from=b; currentMovablePopup=$(pop); showX($(pop)); $('.feedback-button').removeClass('js-active'); b.classList.add('js-active'); 753 var vw=document.documentElement.clientWidth, vh=window.innerHeight, w=pop.offsetWidth, h=pop.offsetHeight, r=b.getBoundingClientRect(), left, top; 754 if (vw<=750) {left=Math.round((vw-w)/2); top=Math.max(r.bottom+8, vh-h-16);} // phone: low on the screen, the text above stays readable 755 else {left=Math.min(Math.max(12, Math.round(r.right-w)), vw-w-12); top=r.bottom+8;} // desktop: under the button, on the card's right side 756 pop.style.left=(left+window.scrollX)+'px'; pop.style.top=(top+window.scrollY)+'px'; 757}, true); 758$(function(){ 759 if (typeof setupMovablePopup==='function') setupMovablePopup('.feedbackPopup','.fbp-tool-button_NOT_USED'); 760 // the drag code moves whichever pop-up is "current": make this one current as soon as it is grabbed 761 var pop=document.querySelector('.feedbackPopup'); if (!pop) return; 762 ['mousedown','touchstart'].forEach(function(t){ pop.addEventListener(t, function(){ currentMovablePopup=$(pop); }, true); }); 763}); 764</script>
764 765 766 767 768 769 770 771 772 773 774 775
776<script> 777/* Star fix (2026-09-28, April: "star on History isn't saved"). basicScripts' setFavorite() (the star in an answer's toolbar on 778 Write/Talk/Read and in History's detail view) sent the page-wide globalRsetIdString/globalCorrIdString, which are only filled after a 779 highlight or a Daily save. With nothing highlighted it sent rsetId=0 and nothing was saved; after highlighting chat A, a star on chat B 780 went to A. This copy is identical except it uses the chat the star sits in (.oneResult rsetId, else the History .oneResultCombo corrId, 781 and the result's corrId); Daily keeps the page-wide ids, as before. Overridden here (loaded after basicScripts) to avoid an asset-version 782 bump; fold it into basicScripts at the next bump. */ 783if (typeof setFavorite==='function') { 784 setFavorite=async function(newVal,x) { 785 setStarState('.oneResult',newVal,x); 786 var setLabel=$(x).closest('.oneResult').attr("rsetId"); 787 if (!setLabel || setLabel==0) {setLabel=$(x).closest('.oneResultCombo').attr("corrId");} 788 var rs=(setLabel && (""+setLabel).indexOf("RS")===0) ? (""+setLabel).substring(2) : "0"; 789 var co=$(x).closest('.oneResult').attr("corrId") || "0000"; 790 if ($('.dailyText').length > 0) { // Daily: the lesson may not be in History yet, so it is saved first (unchanged) 791 if (newVal==true && (!dailySavedToHistory || !globalRsetIdString || !globalCorrIdString)) { 792 const result=await saveNowDailyToHistory(); globalRsetIdString=result.corrId; globalCorrIdString=result.results[1].airId; 793 } 794 rs=globalRsetIdString; co=globalCorrIdString; 795 } 796 const response=await fetch(baseServerURL+"api/apiFavorite.jsp?onOff="+newVal+"&rsetId="+rs+"&corrIds="+co); 797 const data=await response.json(); 798 favArray[nowTabIndex]=newVal; 799 setTabStarClass(nowTabIndex,x); 800 var oneRequestTop=$('.oneRequest[corrid="'+setLabel+'"]'); // the matching row in the History list, if shown 801 setStarState('.oneRequest',(data.aggFave=="true"), oneRequestTop.find('.button-favorite')); 802 if (data.aggFave=="true") {oneRequestTop.addClass('favorite');} else {oneRequestTop.removeClass('favorite');} 803 if (co=="0000") {if (newVal) {oneRequestTop.addClass('favOriginal');} else {oneRequestTop.removeClass('favOriginal');}} 804 }; 805} 806</script>
806 807<!-- tutorial1 HTML START --> 808 809 810<style id="tabbi-welcome"> 811.tutorialPopup {width:min(400px, calc(100vw - 24px)); min-height:0; max-height:calc(100dvh - 32px); padding:24px 22px 20px; box-sizing:border-box; overflow:auto; 812 background:#fff; border:1px solid #e6e6e8; border-radius:12px; color:#211d1e; line-height:1.45; text-align:left; 813 box-shadow:0 22px 52px -20px rgba(0,0,0,.35), 0 5px 14px -8px rgba(0,0,0,.18);} 814/* centred here: the shared script centres by the content width (padding left out), which pushed the card past a phone's edge */ 815.tutorialPopup.tutorialPopup {left:50% !important; transform:translateX(-50%);} 816/* In the visible window, a little above the middle (2026-09-27, April: it sat high on tall screens; the shared script pins top:120px). 817 Fixed, so scrolling does not move it. top 45% with a -45% shift: even a card as tall as the window (max-height below) keeps ~14px 818 top and bottom, so it can never run off a phone's screen. */ 819.tutorialPopup.tutorialPopup {position:fixed !important; top:45% !important; transform:translate(-50%,-45%);} 820.tutorialPopup .popup-close-button {top:12px; right:12px; width:32px; height:32px; border-radius:8px; display:flex; align-items:center; justify-content:center; opacity:.6;} 821.tutorialPopup .popup-close-button:hover {opacity:1; background:#f5f7fa;} 822.tutorialPopup .popup-close-button img {width:16px;} 823.tw-eyebrow {font-size:13px; font-weight:650; color:#bf3143; margin:0 0 6px; padding-right:36px;} 824.tw-title {font-size:1.35rem; font-weight:700; letter-spacing:-.015em; line-height:1.25; margin:0 0 18px; padding-right:24px;} 825.tw-steps {list-style:none; margin:0; padding:0; display:flex; flex-direction:column; gap:14px;} 826.tw-steps li {display:flex; gap:12px; align-items:flex-start; margin:0;} 827.tw-num {flex:0 0 26px; width:26px; height:26px; border-radius:50%; background:#eef1f5; color:#211d1e; font-size:13px; font-weight:700; display:inline-flex; align-items:center; justify-content:center; margin-top:1px;} 828.tw-step-title {display:block; font-size:15px; font-weight:650; line-height:1.35;} 829.tw-step-text {display:block; font-size:14px; line-height:1.45; color:#5d6167; margin-top:2px;} 830.tw-note {margin:18px 0 0; padding:10px 12px; border-radius:8px; background:#f5f7fa; font-size:13.5px; color:#3d3a3b;} 831.tw-steps + .tw-actions {margin-top:20px;} /* members: no note above the buttons */ 832.tw-actions {display:flex; align-items:center; gap:12px; margin-top:16px;} 833.tutorialPopup a.tw-link, .tutorialPopup a.tw-link:link, .tutorialPopup a.tw-link:visited {font-size:14px; color:#5d6167; text-decoration:underline; text-decoration-color:#c9c9c5; text-underline-offset:3px;} /* a:link elsewhere outranked a plain .tw-link */ 834.tutorialPopup a.tw-link:hover {color:#211d1e; text-decoration-color:#211d1e;} 835.tw-go {margin-left:auto; display:inline-flex; align-items:center; height:42px; padding:0 18px; border:0; border-radius:8px; background:#211d1e; color:#fff; font:inherit; font-size:15px; font-weight:650; cursor:pointer; box-shadow:0 6px 16px -8px rgba(33,29,30,.5);} 836.tw-go:hover {background:#2e292a;} 837</style> 838<div class="movablePopup tutorialPopup toolsTarget js-bs-top invisible prevent-select unclicked" staron="false" role="dialog" aria-labelledby="tw-title"> 839 <span class="popup-close-button" onclick='tabbiWelcomeDone();' aria-label="Close"><img class="mainImg" src="images/close-x-trans.png" alt=""></span> 840 <div class="tw-eyebrow">Welcome to Tabbi Talk</div> 841 <div class="tw-title" id="tw-title">Speak, then hear a better way to say it</div> 842 <ol class="tw-steps"> 843 <li><span class="tw-num">1</span><span><span class="tw-step-title">Tap the mic and speak</span><span class="tw-step-text">Up to 2 minutes.</span></span></li> 844 <li><span class="tw-num">2</span><span><span class="tw-step-title">Get feedback</span><span class="tw-step-text">A corrected version, plus tips on grammar and tone.</span></span></li> 845 <li><span class="tw-num">3</span><span><span class="tw-step-title">Compare</span><span class="tw-step-text">Hear the better version, then your own.</span></span></li> 846 847 </ol> 848 <div class="tw-note">5 free chats. No account needed.</div> 849 <div class="tw-actions"> 850 <a class="tw-link" href="how-it-works">How it works</a> 851 <button type="button" class="tw-go" onclick="tabbiWelcomeDone(false);">Start talking</button> 852 </div> 853</div>
854<script> 855/* Once per page per browser (localStorage "tabbiWelcome1" = {write:1, talk:1, ...}). Without storage (private modes that block it), 856 fall back to the old rule: first visit only. Language first (2026-09-27, April): if the native-language question is open (members who 857 have not answered it), the card waits until that question is answered or closed, then opens. It is marked seen only when it shows. */ 858var TW_PAGE='talk', TW_ELIGIBLE=true; 859function tabbiWelcomeDone(startDaily) { 860 closePopupX(".tutorialPopup"); 861 if (startDaily && typeof dailyNow==='function' && !$('.daily-button-wrapper').hasClass('invisible')) {dailyNow();} // Daily: start today's lesson 862} 863function tabbiWelcomeShow() { 864 try {var seen=JSON.parse(localStorage.getItem('tabbiWelcome1')||'{}')||{}; seen[TW_PAGE]=1; localStorage.setItem('tabbiWelcome1', JSON.stringify(seen));} catch (e) {} 865 showPopupX($('.tutorialPopup')); 866} 867$(function(){ setTimeout(function(){ 868 if (!TW_ELIGIBLE) return; 869 var seen={}, stored=true; 870 try {seen=JSON.parse(localStorage.getItem('tabbiWelcome1')||'{}')||{};} catch (e) {stored=false;} 871 if (!stored && !(typeof isNewGuest!=='undefined' && isNewGuest)) return; 872 // No mark = this browser used Tabbi before the cards launched (the header marks a browser only at its first-ever page view). 873 // Mark every card as seen and show nothing, so existing guests and members never get a sudden run of welcome cards. 874 if (stored && localStorage.getItem('tabbiWelcome1')===null) { 875 try {localStorage.setItem('tabbiWelcome1', JSON.stringify({write:1, talk:1, read:1, daily:1, other:1, before:1}));} catch (e) {} 876 return; 877 } 878 if (seen[TW_PAGE]) return; 879 var q=document.querySelector('.surveyPopup'); 880 if (q && !q.classList.contains('invisible') && window.MutationObserver) { 881 var mo=new MutationObserver(function(){ if (q.classList.contains('invisible')) {mo.disconnect(); setTimeout(tabbiWelcomeShow, 250);} }); 882 mo.observe(q, {attributes:true, attributeFilter:['class']}); 883 return; 884 } 885 tabbiWelcomeShow(); 886}, 0); }); 887</script>
887 888 <!-- tutorial END --> 889 890 891 892<!-- shared backdrop for every .popup-message dialog (CSS shows it when one is open) --> 893<div class="popup-scrim" onclick="hideX($('.popup-message'));"></div> 894 895<div class="popup-message prompt-switch-confirm invisible"> 896 <span class="popup-close-button" onclick="hideX($('.popup-message'));"><img src="images/close-x-trans.png"></span> 897 <span class="popup-chip">Detected · <span class='detected-language-name'>Non-English</span></span> 898 <span class="popup-message-title">Translate instead?</span> 899 <span class="popup-message-text">This doesn't look like English.</span> 900 <div class="popup-message-button-area"> 901 <button class="popup-message-button-left pmb-ghost" onclick="hideX($('.popup-message'));">Keep editing</button> 902 <button class="popup-message-button-right pmb-primary" 903 onclick="switchToTranslationPrompt('write_rephrase_edit_translate-eng'); hideX($('.popup-message'));">Translate</button> 904 </div> 905</div> 906<!-- Modal behavior: Esc to close, focus management, focus trap, roles/aria, overlay backdrop-click. 907 Non-invasive: watches .popup-message via MutationObserver; no change to the scattered open-triggers. -->
908<script> 909(function(){ 910 if(window.__pmModalHelper) return; window.__pmModalHelper=true; 911 var lastFocused=null; 912 function focusable(el){ 913 var f=el.querySelectorAll('button, a[href], input, select, textarea, [tabindex]:not([tabindex="-1"])'); 914 return Array.prototype.filter.call(f, function(n){ return n.offsetParent!==null && !n.disabled; }); 915 } 916 function openDialog(el){ 917 try{ 918 el.setAttribute('role','alertdialog'); el.setAttribute('aria-modal','true'); 919 var title=el.querySelector('.popup-message-title, .alert-msg1, .langRedirMessage'); 920 if(title){ if(!title.id){ title.id='pm-t-'+Math.random().toString(36).slice(2,7); } el.setAttribute('aria-labelledby', title.id); } 921 lastFocused=document.activeElement; 922 // Focus the dialog itself (standard dialog pattern), not the primary button: mouse users then see no 923 // keyboard-focus ring on open. Tab still lands on the first button - the container is tabindex=-1 and 924 // excluded from focusable() - and Esc / focus-trap / return-focus behave as before. 925 if(!el.hasAttribute('tabindex')){ el.setAttribute('tabindex','-1'); } 926 setTimeout(function(){ try{ el.focus({preventScroll:true}); }catch(e){ try{ el.focus(); }catch(e2){} } }, 40); 927 }catch(e){} 928 } 929 function closeReturn(){ if(lastFocused && lastFocused.focus){ try{ lastFocused.focus(); }catch(e){} } lastFocused=null; } 930 function observe(el){ 931 var open=!el.classList.contains('invisible'); if(open){ openDialog(el); } 932 new MutationObserver(function(){ 933 var now=!el.classList.contains('invisible'); 934 if(now && !open){ openDialog(el); } else if(!now && open){ closeReturn(); } 935 open=now; 936 }).observe(el,{attributes:true, attributeFilter:['class']}); 937 } 938 function openEl(){ return document.querySelector('.popup-message:not(.invisible), .overlay-join:not(.invisible), .overlay-subscribe:not(.invisible)'); } 939 document.addEventListener('DOMContentLoaded', function(){ 940 document.querySelectorAll('.popup-message').forEach(observe); 941 }); 942 // clicking an overlay's own dark area (not the card) dismisses it 943 document.addEventListener('click', function(e){ 944 if(e.target.classList && (e.target.classList.contains('overlay-join')||e.target.classList.contains('overlay-subscribe'))){ try{ hideX($(e.target)); }catch(err){} } 945 }); 946 // Esc closes; Tab stays trapped inside the open dialog 947 document.addEventListener('keydown', function(e){ 948 var dlg=openEl(); if(!dlg) return; 949 if(e.key==='Escape'){ try{ hideX($('.popup-message')); hideX($('.overlay-join')); hideX($('.overlay-subscribe')); }catch(err){} return; } 950 if(e.key==='Tab'){ 951 var f=focusable(dlg); if(!f.length) return; 952 var first=f[0], last=f[f.length-1]; 953 if(e.shiftKey && document.activeElement===first){ e.preventDefault(); last.focus(); } 954 else if(!e.shiftKey && document.activeElement===last){ e.preventDefault(); first.focus(); } 955 } 956 }); 957})(); 958</script>
958 959 960 961<style id="alert-phone-fit"> 962/* Phones (2026-09-24): style<VER>.css still carries the pre-redesign .area-alert look under 750px (300px, 7px corners, white, thin text). Match the desktop card and keep it inside the screen. */ 963@media only screen and (max-width: 750px) { 964 .area-alert {box-sizing:border-box; width:min(392px, calc(100vw - 28px)); padding:26px 24px 22px; border-radius:12px; 965 background-color:#fff; border:1px solid #e6e6e8; color:#211d1e; font-size:.95rem; line-height:1.4; font-weight:inherit; z-index:1200; 966 box-shadow:0 22px 52px -20px rgba(0,0,0,.35), 0 5px 14px -8px rgba(0,0,0,.18);} 967} 968/* Neutral backdrop and a white card, like the style chooser and the dialogs (April: "I don't like red color overlay ... match with that"). 969 style<VER>.css gives .overlay a reddish rgba(38,20,23,.46) and .area-alert a pink card. */ 970.overlay-join, .overlay-subscribe {background-color:rgba(20,20,22,.42);} 971.overlay-join .area-alert, .overlay-subscribe .area-alert {text-align:left; background-color:#fff; border:1px solid #e6e6e8; box-shadow:0 22px 52px -20px rgba(0,0,0,.35), 0 5px 14px -8px rgba(0,0,0,.18);} 972/* Left-aligned like every other pop-up (History unlock, dialogs, Ask Tabbi, quiz, welcome, contact); these two were the only centred ones 973 (April: "is it to be center aligned? considering other similar modal?"). */ 974.overlay-join .area-alert .popup-chip, .overlay-subscribe .area-alert .popup-chip {margin-left:0;} 975.overlay-join .area-alert .overlay-body, .overlay-subscribe .area-alert .overlay-body {padding:0;} 976.overlay-join .area-alert .alert-msg1, .overlay-subscribe .area-alert .alert-msg1 {padding-left:0; padding-right:0;} 977/* The Plus button is a link, and a site link colour turned its text dark on the red (April: "color is wrong"): white like Join Tabbi. */ 978a.button-popup, a.button-popup:link, a.button-popup:visited, a.button-popup:hover {color:#fff; text-decoration:none;} 979</style> 980<div class="overlay-join overlay invisible"> 981 <div class="area-alert"> 982 <span class="button-close-overlay" onclick="hideX($('.overlay-join'));">×</span> 983 <span class="popup-chip gate">Free account</span> 984 985 <span class="alert-msg1">You've used your 5 free chats</span> 986 <p class="overlay-body">Join free for 10 chats a day. Your History and streak are saved.</p> 987 <span class="button-join-overlay" onclick='window.location.assign("https://auth.tabbi.ai/signup?client_id=34t7p7b7r0sc2799h1rcvjksse&response_type=code&scope=email+openid+phone&redirect_uri=https%3A%2F%2Ftabbi.ai%2Fauth.jsp");'>Join Tabbi</span> 988 <span class="link-login-overlay">Already have an account? <a href="https://auth.tabbi.ai/login?client_id=34t7p7b7r0sc2799h1rcvjksse&response_type=code&scope=email+openid+phone&redirect_uri=https%3A%2F%2Ftabbi.ai%2Fauth.jsp">Log in</a></span> 989 <span class="alert-msg2">By joining, you agree to the <a target="_blank" href="terms">Terms of Use</a> and <a target="_blank" href="privacy">Privacy Policy</a>.</span> 990 </div> 991</div> 992 993<div class="overlay-subscribe overlay invisible"> 994 <div class="area-alert"> 995 <span class="button-close-overlay" onclick="hideX($('.overlay-subscribe'));">×</span> 996 <span class="popup-chip warn">Daily limit</span> 997 998 <span class="alert-msg1">You've used your 10 free chats</span> 999 1000 <p class="overlay-body">Free accounts can send 10 chats in 24 hours. Try again later, or get unlimited chats with Tabbi Plus.</p> 1001 <a href="plans" class="button-popup">Try Plus free for 14 days</a> 1002 </div> 1003</div> 1004 1005 1006 1007
1008<script> $(function(){ 1009 onLoadAllPagesNew(null, null); 1010 /* onLoadTalkPage */ 1011 onLoadInputPage("TALK_PAGE", 2); 1012 $(".quiz-onoff").remove(); 1013 /* removed customerio stuff cio. */ 1014 let isFirefox = typeof InstallTrigger !== 'undefined'; 1015 if (isFirefox) {$($('.js-area-input')).attr('contenteditable', 'true');} 1016 $('.mic-play-combo').removeClass('invisible'); 1017}); 1018</script>
1018 1019 1020<div class='main-box talk-page'> 1021 1022 1023
1024<script> 1025let numSelected=3; 1026let selectedChanged=false; 1027let promptModeFilter='talk'; 1028let promptListCSV_write="write_rephrase_edit_corrected,write_rephrase_edit_eloquent,write_rephrase_coach_feedback"; 1029let promptListCSV_talk="talk_rephrase_edit_corrected,talk_rephrase_edit_eloquent,talk_rephrase_coach_feedback"; 1030currentlySelectedPromptListCSV="talk_rephrase_edit_corrected,talk_rephrase_edit_eloquent,talk_rephrase_coach_feedback"; 1031</script>
1031 1032 1033 1034<style id="ai-style-chooser"> 1035.overlay-prompt-chooser.overlay {background-color:rgba(20,20,22,.42);} 1036.overlay-prompt-chooser .prompt-menu {display:flex; flex-direction:column; width:min(440px, calc(100vw - 24px)); max-width:none; max-height:min(82vh, 680px); 1037 padding:0; background:#fff; border:1px solid #e6e6e8; border-radius:12px; color:#211d1e; overflow:hidden; 1038 box-shadow:0 22px 52px -20px rgba(0,0,0,.35), 0 5px 14px -8px rgba(0,0,0,.18);} 1039.overlay-prompt-chooser .prompt-menu-header {position:relative; height:auto; display:block; padding:20px 56px 6px 22px; background:#fff; justify-content:flex-start;} 1040.overlay-prompt-chooser .prompt-menu-title {margin:0; font-size:1.2rem; line-height:1.3; font-weight:700; letter-spacing:-.015em; text-align:left;} 1041.overlay-prompt-chooser .button-close-prompt-menu {position:absolute; top:14px; right:14px; margin:0; transform:none; width:32px; height:32px; border-radius:8px; 1042 display:flex; align-items:center; justify-content:center; opacity:.6;} 1043.overlay-prompt-chooser .button-close-prompt-menu:hover {opacity:1; background:#f5f7fa;} 1044.overlay-prompt-chooser .button-close-prompt-menu img {height:16px; width:16px;} 1045@media (hover:none) {.overlay-prompt-chooser .button-close-prompt-menu {width:40px; height:40px; top:10px; right:10px;}} /* bigger tap target on phones */ 1046.overlay-prompt-chooser .prompt-menu-subtitle {padding:14px 22px 6px; font-size:12px; font-weight:650; letter-spacing:.04em; text-transform:uppercase; color:#736769;} 1047.overlay-prompt-chooser .prompt-menu-list {overflow:visible; padding:0 12px 4px; flex:0 0 auto;} 1048.overlay-prompt-chooser .prompt-menu-list.style-list {overflow-y:auto; flex:1 1 auto; min-height:0;} /* only the style list scrolls */ 1049.overlay-prompt-chooser .prompt-menu-item {display:flex; align-items:center; gap:12px; margin:0 0 4px; padding:11px 12px; text-indent:0; 1050 border:1.5px solid transparent; border-radius:8px; background:#fff; color:#211d1e; font-size:15px; font-weight:500; line-height:1.3; cursor:pointer;} 1051.overlay-prompt-chooser .prompt-menu-item:last-of-type {border-bottom:1.5px solid transparent;} 1052.overlay-prompt-chooser .prompt-menu-item:hover {background:#f5f7fa; font-weight:500;} 1053.overlay-prompt-chooser .prompt-menu-item .ddIconSpan {flex:0 0 22px; text-align:center; font-size:17px; line-height:1;} 1054.overlay-prompt-chooser .prompt-menu-item .ddTitleSpan {flex:1 1 auto;} 1055.overlay-prompt-chooser .prompt-menu-item.selected {background:#fff; border-color:#211d1e;} 1056.overlay-prompt-chooser .prompt-menu-item.selected::after {content:""; flex:0 0 20px; height:20px; border-radius:50%; background:#211d1e center / 11px 11px no-repeat; 1057 background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3Cpath d='M2.5 6.2l2.3 2.3 4.7-4.9' fill='none' stroke='white' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");} 1058.overlay-prompt-chooser .prompt-menu-item.pinned, .overlay-prompt-chooser .prompt-menu-item.pinned:hover {background:#f5f7fa; border-color:transparent; color:#3d3a3b; cursor:default;} 1059.overlay-prompt-chooser .prompt-menu-item.pinned::after {display:none;} 1060.overlay-prompt-chooser .dd-pin {flex:0 0 auto; font-size:12px; font-weight:600; color:#5d6167; background:#fff; border:1px solid #e6e6e8; border-radius:999px; padding:3px 9px;} 1061.overlay-prompt-chooser .prompt-menu-list.style-list {padding-bottom:12px;} /* no footer: a little room below the last style */ 1062</style> 1063<div class='overlay-prompt-chooser overlay talk-prompts invisible'> 1064 <div class='prompt-menu'> 1065 <div class="prompt-menu-header"> 1066 <span class="prompt-menu-title">Select AI Style</span> 1067 <span class='float-right button button-close-prompt-menu' onclick='togglePromptChooser();'><img src='images/close-x-trans.png'></span> 1068 </div> 1069 1070 <div class="prompt-menu-subtitle invisible"><span class='prompt-count'>3</span> selected</div> 1071 <div class="prompt-menu-subtitle">Always included</div> 1072 <div class='prompt-menu-list'> 1073 1074 <div class='prompt-menu-item selected pinned' 1075 value="talk_rephrase_edit_corrected" >
1076 <span class='ddIconSpan'>âï¸</span> <span class='ddTitleSpan'>Minimal Correction</span><span class='dd-pin'>Always on</span> 1077 </div> 1078 </div> 1079 <div class="prompt-menu-subtitle">Style</div> 1080 <div class='prompt-menu-list style-list'> 1081 1082 1083 <div class='style-prompt prompt-menu-item selected' 1084 onclick='togglePChoice(this);' value="talk_rephrase_edit_eloquent" > 1085 <span class='ddIconSpan'>ð</span> <span class='ddTitleSpan'>Eloquent Correction</span> 1086 </div> 1087 <div class='style-prompt prompt-menu-item' 1088 onclick='togglePChoice(this);' value="talk_rephrase_edit_casual" > 1089 <span class='ddIconSpan'>ð</span> <span class='ddTitleSpan'> Casual Correction</span> 1090 </div> 1091 <div class='style-prompt prompt-menu-item' 1092 onclick='togglePChoice(this);' value="talk_rephrase_edit_interview-response-STAR" > 1093 <span class='ddIconSpan'>ð£ï¸</span> <span class='ddTitleSpan'>Job Interview Response</span> 1094 </div> 1095 <div class='style-prompt prompt-menu-item' 1096 onclick='togglePChoice(this);' value="multi_summarize_edit_summary" > 1097 <span class='ddIconSpan'>â</span> <span class='ddTitleSpan'>Shorten</span> 1098 </div> 1099 <div class='style-prompt prompt-menu-item' 1100 onclick='togglePChoice(this);' value="multi_rephrase_edit_persuade" > 1101 <span class='ddIconSpan'>ð¤</span> <span class='ddTitleSpan'>Persuasive Tone</span> 1102 </div> 1103 </div> 1104 1105 <div class="prompt-menu-subtitle invisible">[Feedback]</div> 1106 <div class='prompt-menu-list invisible'> 1107 <div class="prompt-menu-item selected" onclick="togglePChoice(this);" value="write_rephrase_coach_feedback"><span class="ddIconSpan">ð©âÂÂð«</span> <span class="ddTitleSpan">Writing Feedback</span></div> 1108 </div> 1109 1110 1111 </div> 1112</div> 1113 1114<div class='button-prompt-chooser' onclick='togglePromptChooser();'> 1115 <span class='prompt-select-title'>AI Style: <span class='selected-prompt-mode'></span></span> 1116</div> 1117
1118<script>(function(){var s=document.querySelector('.style-prompt.selected'), t=document.querySelector('.selected-prompt-mode'); if (s && t) t.innerHTML=s.innerHTML;})();</script>
1118 1119 1120<!-- END: Above is the code for prompt selection --> 1121 1122<style> 1123 .oneRowFlexParent {display: flex; flex-direction: row; justify-content: space-evenly; align-items: stretch;} 1124 .oneColumnFlexParent {display: flex; flex-direction: column; justify-content: space-evenly; align-items: stretch;} 1125 .showAsFlexChild {flex-grow: 1; flex-basis: 0; display: flex;} 1126</style> 1127 1128 1129 1130<div class='oneRequest'> 1131<div class="area-main1 js-roof-node tabbed-set-holder"> 1132 <div class='js-tabtopbar-talk invisible'> 1133 <div class='tabtop0 original-tab clickable js-tabtop js-tabtop-talk js-selected' 1134 onclick='showOneTab(this,"oneResult","oneResult-original");nowTabIndex=0;submitActive(true);'> 1135 Original</div> 1136 <div class='tabtop1 corrected-tab clickable js-tabtop js-tabtop-talk' 1137 onclick='showOneTab(this,"oneResult","oneResult-corrected");nowTabIndex=1;submitActive(false);'> 1138 Corrected</div> 1139 <div class='tabtop2 eloquent-tab clickable js-tabtop js-tabtop-talk' 1140 onclick='showOneTab(this,"oneResult","oneResult-eloquent");nowTabIndex=2;submitActive(false);'> 1141 Edited</div> 1142 </div> 1143 1144 <div class="area-audiogroup oneResultCombo"> 1145 1146 <div class="area-main3"> 1147 1148 1149 <div class="input-mode-area talk-mode "> 1150 <div class="js-bs-top oneResult oneResult-original original-text js-tab-main content-set before-submit"> 1151 <div class="area-talk-input js-area-input copyable" contentEditable="plaintext-only" oninput="onTextInput(this);"> 1152 <div class='cursor-talk-input'></div> 1153 </div> 1154 <span class="button-big-mic button-state js-active"><img class="bigmicrophone" src="images/bigmic.png" width="200" alt="Start"/></span> 1155 </div> 1156 <div class="area-tools-combo1 status-message"></div> 1157 </div> 1158 1159 1160 1161 1162 </div> 1163 1164 <div class="area-buttons-talk button-set1"> 1165 <span class="button-reset-text button-state"></span> 1166 <span class=" talk-mode button-toggle-voice button-state js-active"></span> 1167 <span class="button-submit-talk button-state"></span> 1168 </div> 1169 <div class="area-buttons-talk button-set2"> 1170 <span class="button-restart-text button-state js-active"></span> 1171 <span class="button-submit-talk button-state"></span> 1172 </div> 1173 </div> 1174</div> 1175</div> 1176 1177</div> 1178 1179 1180 1181<!-- agreement (consent dialog) start --> 1182 1183 <div class="area-agreement popup-message agreement-dialog invisible"> 1184 <span class="popup-close-button" onclick="hideX($('.popup-message'));"><img src="images/close-x-trans.png"></span> 1185 <span class="popup-message-title">Create your account</span> 1186 <span class="popup-message-text">By continuing, you agree to Tabbi's <a target="_blank" href="terms">Terms</a> and <a target="_blank" href="privacy">Privacy</a>.</span> 1187 <div class="popup-message-button-area"> 1188 <button type="button" class="popup-message-button-left pmb-ghost" onclick="hideX($('.popup-message'));">Cancel</button> 1189 <button type="button" class="popup-message-button-right pmb-ink" onclick='catoutThenLink("AGREEMENT: I understand","https://auth.tabbi.ai/signup?client_id=34t7p7b7r0sc2799h1rcvjksse&response_type=code&scope=email+openid+phone&redirect_uri=https%3A%2F%2Ftabbi.ai%2Fauth.jsp%3Fpage=talk");'>Continue to sign up</button> 1190 </div> 1191 </div> 1192 <!-- agreement end --> 1193 1194</body> 1195 1196 1197</html> 1198 1199 1200<style> 1201@media (hover:hover) { 1202 [data-tip] {position:relative;} 1203 .tool-box span[data-tip] {display:inline-block;} 1204 [data-tip]::after {content:attr(data-tip); position:absolute; left:50%; bottom:calc(100% + 8px); transform:translateX(-50%); 1205 background:#211d1e; color:#fff; font-size:12px; line-height:1; font-weight:500; font-style:normal; letter-spacing:normal; text-transform:none; text-align:left; 1206 padding:6px 9px; border-radius:6px; white-space:nowrap; pointer-events:none; opacity:0; transition:opacity .12s ease .6s; z-index:50;} 1207 [data-tip]:hover::after {opacity:1;} 1208 .tt-warm [data-tip]:hover::after {transition-delay:0s;} 1209 [data-tip][data-tip-off]::after {display:none;} 1210 .tip-below[data-tip]::after {bottom:auto; top:calc(100% + 8px);} 1211 .tip-right[data-tip]::after {left:auto; right:0; transform:none;} 1212 .tip-left[data-tip]::after {left:0; transform:none;} 1213 /* the star opens the toolbar and Copy closes it: open inward so the answer card never cuts them off */ 1214 .tool-box .button-favorite[data-tip]::after {left:0; transform:none;} 1215 .tool-box .button-copy[data-tip]::after {left:auto; right:0; transform:none;} 1216 /* The Read highlight pop-up (.snippetOptions) scrolls (overflow:auto) and its toolbar sits at its top edge, so a tip drawn above an 1217 icon would be cut off. There the tip is placed against the window instead (position:fixed; --tt-x/--tt-y set by the script on hover), 1218 so the pop-up's edge cannot clip it. The pop-up is moved with left/top, never a transform, which keeps fixed = window. */ 1219 .snippetOptions .tool-box [data-tip]::after {position:fixed; left:var(--tt-x,0); top:var(--tt-y,0); right:auto; bottom:auto; transform:translate(-50%, calc(-100% - 8px));} 1220 /* first and last icons open inward, so a pop-up near the window's edge keeps them on screen */ 1221 .snippetOptions .tool-box > .clickable:first-child [data-tip]::after {transform:translate(-18px, c
1221alc(-100% - 8px));} 1222 .snippetOptions .tool-box > .clickable:last-child [data-tip]::after {transform:translate(calc(-100% + 18px), calc(-100% - 8px));} 1223} 1224/* "Copied" (2026-09-27, April): tapping Copy turns its tip into "Copied" for 1.5 s, in the same ink pill and the same spot. 1225 Outside the hover query on purpose: phones have no tips, and this is their only sign the copy worked. 1226 Shows even when the Copy tip has retired. Replaces the old grey .guide-copied label in toolbars. */ 1227.tool-box .guide-copied {display:none !important;} 1228.tool-box .button-copy.tt-flash {position:relative; display:inline-block;} 1229.tool-box .button-copy.tt-flash::after {content:'Copied'; display:block; position:absolute; top:auto; left:auto; right:0; bottom:calc(100% + 8px); transform:none; 1230 background:#211d1e; color:#fff; font-size:12px; line-height:1; font-weight:500; font-style:normal; letter-spacing:normal; text-transform:none; 1231 padding:6px 9px; border-radius:6px; white-space:nowrap; pointer-events:none; opacity:1; transition:none; z-index:50;} 1232.snippetOptions .tool-box .button-copy.tt-flash::after {position:fixed; left:var(--tt-x,0); top:var(--tt-y,0); right:auto; bottom:auto; transform:translate(-50%, calc(-100% - 8px));} 1233</style>
1234<script> 1235// Inside the highlight pop-up, tips are placed against the window: remember where the icon is (see the style above). 1236function ttPlaceFixed(el) { 1237 if (!el || !el.closest || !el.closest('.snippetOptions')) return; 1238 var r=el.getBoundingClientRect(); el.style.setProperty('--tt-x', (r.left+r.width/2)+'px'); el.style.setProperty('--tt-y', r.top+'px'); 1239} 1240(function(){ 1241 // Copy -> "Copied" for 1.5 s, on every device (see the style above). 1242 if (document.addEventListener) document.addEventListener('click', function(e) { 1243 var b=e.target && e.target.closest ? e.target.closest('.tool-box .button-copy') : null; if (!b) return; 1244 ttPlaceFixed(b); b.classList.add('tt-flash'); clearTimeout(b.ttFlash); b.ttFlash=setTimeout(function(){b.classList.remove('tt-flash');}, 1500); 1245 }, true); 1246})(); 1247(function(){ 1248 if (!window.matchMedia || !window.matchMedia('(hover:hover)').matches || !document.addEventListener) return; 1249 var LEARN=3, STORE='tabbiTips1', WAIT=600, COOL=800; 1250 var TT_KEEP=/Free account needed|Record first/; 1251 var seen={}; 1252 try {seen=JSON.parse(localStorage.getItem(STORE)||'{}')||{};} catch (e) {} 1253 function save() {try {localStorage.setItem(STORE, JSON.stringify(seen));} catch (e) {}} 1254 1255 // [selector, learning key, 'use' = retires after 3 clicks | 'view' = retires after shown 3 times] 1256 var KEYS=[ 1257 ['#toolbox-star','highlight','use'], ['.button-meaning','meaning','use'], ['.button-rephrase','rephrase','use'], ['.button-native','native','use'], 1258 ['.button-favorite, .hx-row-star .img-button','star','use'], ['.button-read-aloud','listen','use'], 1259 ['.button-audio-mphone, .mic-in-toolbar','record','use'], ['.button-audio-playback','playback','use'], 1260 ['.button-translate','translate','use'], ['.button-diff-show','changes','use'], ['.button-copy','copy','use'], 1261 ['.dailyArrow','day','use'], ['.dailyLink','source','use'], ['.hx-del','delete','use'], 1262 ['.hx-search-btn, .hx-search-clear','search','use'], ['.hx-star-toggle','only-starred','use'], ['.hx-hl-toggle','only-highlighted','use'], 1263 ['.backButton','back','use'], ['.hx-pill-hl','highlighted','view'], ['.streakLeft','streak','view'], ['.streakRight','dots','view']]; 1264 function keyOf(el) { 1265 for (var i=0;i<KEYS.length;i++) {if (el.matches(KEYS[i][0])) return {k:KEYS[i][1], kind:KEYS[i][2]};} 1266 return {k:'tip:'+(el.getAttribute('data-tip')||''), kind:'use'}; 1267 } 1268 function learned(el, info) {return !TT_KEEP.test(el.getAttribute('data-tip')||'') && (seen[info.k]||0)>=LEARN;} 1269 1270 // Answer-toolbar labels, read from the icon's state at the moment the pointer arrives. 1271 function shown(b, sel) {var i=b.querySelector(sel); return !!(i && !i.classList.contains('invisible'));} 1272 function member() {return (typeof userIsMember==='undefined') || !!userIsMember;} 1273 var TOOL=[ 1274 // Read highlight pop-up: its first button reuses .button-favorite but saves a highlight (toggleStarGoldSnippet) 1275 ['#toolbox-star', function(b){var p=b.closest('.toolsTarget'); var on=p ? p.getAttribute('starOn')=='true' : shown(b,'.image-two'); return on ? 'Highlighted. Tap to remove' : 'Highlight it';}], 1276 ['.button-meaning', function(){return 'Explain the meaning';}], 1277 ['.button-rephrase', function(){return 'Say it another way';}], 1278 ['.button-native', function(){return 'See it in your language';}], 1279 ['.button-favorite', function(b){var r=b.closest('.oneResult'); var on=r ? r.getAttribute('starOn')=='true' : shown(b,'.image-two'); return on ? 'Starred. Tap to remove' : 'Star to find it later';}], 1280 ['.button-read-aloud', function(b){return shown(b,'.image-two') ? 'Pause' : 'Hear it read aloud';}], 1281 ['.button-audio-mphone', function(b){return shown(b,'.image-two') ? 'Stop recording' : 'Record yourself saying it';}], 1282 ['.button-audio-playback', function(b){return shown(b,'.image-four') ? 'Record first, then hear yourself' : (shown(b,'.image-three') ? 'Pause' : 'Play your recording');}], 1283 ['.button-translate', function(b){return !member() ? 'See it in your language. Free account needed' : (shown(b,'.image-two') ? 'Back to English' : 'See it in your language');}], 1284 ['.button-diff-show', function(b){return shown(b,'.image-three') ? '' : (shown(b,'.image-two') ? 'Hide the changes' : 'Show what changed');}], 1285 ['.button-copy', function(){return 'Copy';}]]; 1286 function labelToolbar(t) { 1287 var box=t.closest('.tool-box'); if (!box) return; 1288 for (var i=0;i<TOOL.length;i++) { 1289 var b=t.closest(TOOL[i][0]); if (!b || !box.contains(b)) continue; 1290 if (b.hasAttribute('data-tip') && !b.hasAttribute('data-tip-auto')) return; // the page set its own words (Daily) 1291 var text=TOOL[i][1](b); 1292 if (text) {b.setAttribute('data-tip', text); b.setAttribute('data-tip-auto','');} else {b.removeAttribute('data-tip');} 1293 return; 1294 } 1295 } 1296 1297 var root=document.documentElement, cur=null, showT=null, coolT=null; 1298 document.addEventListener('mouseover', function(e) { 1299 var t=e.target; if (!t || !t.closest) return; 1300 labelToolbar(t); 1301 var el=t.closest('[data-tip]'); 1302 if (el===cur) return; 1303 cur=el; clearTimeout(showT); 1304 if (!el) return; 1305 clearTimeout(coolT); 1306 ttPlaceFixed(el); 1307 var info=keyOf(el); 1308 if (learned(el, info)) {el.setAttribute('data-tip-off',''); return;} 1309 el.removeAttribute('data-tip-off'); 1310 var warm=root.classList.contains('tt-warm'); 1311 showT=setTimeout(function(){ 1312 if (cur!==el) return; 1313 root.classList.add('tt-warm'); 1314 if (info.kind=='view') {seen[info.k]=(seen[info.k]||0)+1; save();} 1315 }, warm ? 0 : WAIT); 1316 }, true); 1317 document.addEventListener('mouseout', function(e) { 1318 if (!cur) return; 1319 if (e.relatedTarget && cur.contains(e.relatedTarget)) return; 1320 var r=e.relatedTarget && e.relatedTarget.closest ? e.relatedTarget.closest('[data-tip]') : null; 1321 if (r) return; // moving straight onto another tip: the mouseover above takes it 1322 cur=null; clearTimeout(showT); 1323 clearTimeout(coolT); coolT=setTimeout(function(){root.classList.remove('tt-warm');}, COOL); 1324 }, true); 1325 // Using a control hides its tip at once (they just acted) and counts toward retiring it. 1326 document.addEventListener('click', function(e) { 1327 var el=e.target && e.target.closest ? e.target.closest('[data-tip]') : null; if (!el) return; 1328 el.setAttribute('data-tip-off',''); 1329 var info=keyOf(el); if (info.kind!='use') return; 1330 seen[info.k]=(seen[info.k]||0)+1; save(); 1331 }, true); 1332})(); 1333</script>
1333 1334 1335<style> 1336/* Toolbox row (2026-09-25, April: the mic/play pill and its icons sat ~3px high on Daily). In standards mode the inline-block 1337 icon spans sit on the baseline, so each cell is ~6px taller than its 26px icon; flex-centre the cells and the pill instead. 1338 The :not() guards keep .invisible and the always-hidden .mic-in-toolbar at display:none (this rule outranks both). */ 1339.tool-box > .clickable:not(.invisible):not(.mic-in-toolbar), .tool-box .mic-play-combo:not(.invisible), .tool-box .mic-play-combo > .clickable:not(.invisible) {display:flex; align-items:center; justify-content:center;} 1340/* The highlighter icon beside a chat title is 20x21; style0926.css pins .has-highlights to 16x16, which squeezed it (2026-09-26). */ 1341img.has-highlights {height:auto;} 1342/* Width cap, Read guide bar and the tab band moved to WEB-INF/tabbiPageLayout.inc (loaded in the header, before the page box). */ 1343</style> 1344<div class='template invisible js-bs-top oneRequest locked'> 1345<div class="blur-overlay"></div> <img src="images/locked.png" class="locked-icon"> 1346<div class="requestBasicsAll"> 1347 <div class="requestBasics1 button-favorite center"> 1348 <span class='img-button clickable' onclick="toggleFavoriteReq(this);"> 1349 <img width="25px;" class="icon-multi image-one" src="images/star.png"> 1350 <img width="25px;" class="icon-multi image-two invisible" src="images/star-selected.png"> 1351 </span> 1352 </div> 1353 <div class="requestBasics1 button-highlight center invisible"> 1354 <span class='img-button clickable' onclick="toggleHighlightReq(this);"> 1355 <img width="25px;" class="icon-multi image-one invisible" src="images/highlight-black.png"> 1356 <img width="25px;" class="icon-multi image-two" src="images/highlight-yellow.png"> 1357 </span> 1358 </div> 1359 <div class='requestBasics' onclick='clickRequestBasics(this);' > 1360 <span class='summarySpan'> 1361 <span class='summary-img invisible'><img src='images/challenge.png' width=16/></span> 1362 <span class='title'><span class='titleText'>GENERATED TITLE</span><img class="has-highlights invisible" src="images/highlight-cut.png"></span> 1363 </span> 1364 <span class='inputSpan'>INPUT</span> 1365 <div class="info-row"> 1366 1367 <span class="timestampSpan">TIME</span> · <span class="request-mode"><img width="12px" src="images/talk-history.png"></span> · <span class="mode-description">Tabbi Correction</span> · <span class="wordcountSpan">0 words</span> 1368 </div> 1369 </div> 1370 <div class='requestSnippets invisible' ></div> 1371 <div class="requestBasics2 center"> 1372 <span class='img-button clickable' onclick="toggleCorr(this,'oneRequest');"> 1373 <img class="history-delete gray-icon" width=25px; src='images/delete-black.png'/> 1374 <img class="history-delete red-icon invisible" width=25px; src='images/delete-red.png'/> 1375 </span> 1376 </div> 1377</div> 1378</div> 1379 1380<div class='template invisible js-bs-top oneResult' rsetId=0 corrId=0 diffOn=false starOn=false> 1381 1382 <div class='js-tab-main content-set'> 1383 1384 <div class='cell-title'> 1385 <span class='iconSpan'>OOOO</span> <span class="titleSpan">TITLE</span> 1386 <span class='button feedback-button invisible'>Feedback</span> 1387 </div> 1388 1389 <div class='result-text'><div class='result-content copyable'> 1390 <span class="contentSpan"><span class='temp-text'>Content</span></span> 1391 </div></div> 1392 1393 <div class='result-diff invisible' onclick='setDiffState(false,this);'><div class='result-content'> 1394 <span class="diffSpan"><span class='temp-text'>DIFF</span></span> 1395 </div></div> 1396 1397 <div class='area-tools-combo2'> 1398 <div class='tool-box'> 1399 <div class='clickable'><span class='button-favorite' onclick='toggleFavorite(this);'><img class='icon-multi image-one' src='images/star.png'><img class='icon-multi image-two invisible' src='images/star-selected.png'></span></div> 1400 1401 <div class='clickable'><span class='button-read-aloud' onclick='buttonReadAloud(this);'> 1402 <img class='icon-multi image-one' src='images/read.png'> 1403 <img class='icon-multi image-two invisible' src='images/read-selected.png'> 1404 </span></div> 1405 1406 <div class='clickable mic-in-toolbar'><img class='icon-multi image-one' src='images/mic-black.png'><img class='icon-multi image-two invisible' src='images/mic-red.png'></div> 1407 1408 <div class='mic-play-combo invisible'> 1409 <div class='clickable mphone-div invisible'><span class='button-audio-mphone' onclick='toggleRecording(this);'> 1410 <img class='icon-multi image-one' src='images/mic-black.png'> 1411 <img class='icon-multi image-two invisible' src='images/mic-red.png'> 1412 </span></div> 1413 <div class='clickable playback-div invisible'><span class='button-audio-playback' onclick='togglePlayback(this);'> 1414 <img class='icon-multi image-one' src='images/play-black.png'> 1415 <img class='icon-multi image-two invisible' src='images/play-red.png'> 1416 <img class='icon-multi image-three invisible' src='images/pause-red.png'>
1417 <img class='icon-multi image-four invisible' src='images/play-gray.png'> 1418 </span></div> 1419 </div> 1420 1421 1422 <div class='clickable diff-in-toolbar'><span class='button-diff-show'><img class='icon-multi image-one' src='images/diff.png' ><img class='icon-multi image-two invisible' src='images/diff-selected.png' ><img class='icon-multi image-three invisible' src='images/diff-disabled.png' ></span></div> 1423 <div class='clickable copy-position'><span class='guide-copied'>Copied</span><span class='button-copy' onclick='clipboardHTarget(this,"oneResult","result-content");'><img class='icon-multi image-one' src='images/copy.png' ><img class='icon-multi image-two invisible' src='images/copy-selected.png' ></span></div> 1424 1425 </div> 1426 </div> 1427 </div> 1428 1429</div>
1430<script> 1431let translationRetrieved=false; 1432let translationInProgress=false; 1433let transOn=false; 1434async function showTranslatedTextAsDiff(buttonX) { 1435 let buttonJQ=$(buttonX); 1436 let fromHistory=$(".history-pagination").length>0; 1437 if (fromHistory) { 1438 translationRetrieved=buttonJQ.closest(".oneResult").attr('translationRetrieved'); 1439 translationInProgress=buttonJQ.closest(".oneResult").attr('translationInProgress'); 1440 } 1441 if (!userIsMember) {showX($('.popup-message.translate-access')); return;} 1442 transOn=!transOn; 1443 if (!translationInProgress && !translationRetrieved) { 1444 translationInProgress=true; if (fromHistory) {buttonJQ.closest(".oneResult").attr('translationInProgress',true);} 1445 buttonJQ.closest(".oneResult").find(".diffSpan").html("<div class='fulltext-translation'>Translating...</div>"); 1446 toggleDiff(buttonX); 1447 try { 1448 let x=getTargetTextJQ(buttonX,"oneResult","result-content"); 1449 let xHtml= x.find(".contentSpan").html(); 1450 if (x.find('.dailyText').length>0) {xHtml=x.find('.dailyTitle').html()+" --- "+x.find('.dailyText').html();} 1451 console.log("To be translated: "+xHtml); 1452 let properText= xHtml 1453 .replace(/ /gi, ' ') 1454 .replace(/<br\s*\/?>/gi, '\n') 1455 .replace(/<p>([\s\S]*?)<\/p>/g, '$1\n\n') 1456 .replace(/<[^>]*>/g, '') 1457 .replace(/\n{3,}/g, '\n\n') 1458 .trim(); 1459 let translatedText= await fetchTranslatedText(properText); 1460 buttonJQ.closest(".oneResult").find(".diffSpan").html("<div class='fulltext-translation'>"+translatedText+"</div>"); 1461 translationRetrieved=true; if (fromHistory) {buttonJQ.closest(".oneResult").attr('translationRetrieved',true);} 1462 } catch (e) { // [fix] was stuck on "Translating..." forever + all translate buttons dead 1463 console.log("translate error:",e); 1464 buttonJQ.closest(".oneResult").find(".diffSpan").html("<div class='fulltext-translation'>Translation unavailable. Please try again.</div>"); 1465 } finally { 1466 translationInProgress=false; if (fromHistory) {buttonJQ.closest(".oneResult").attr('translationInProgress',false);} 1467 } 1468 } else {toggleDiff(buttonX);} 1469} 1470async function fetchTranslatedText(text) { 1471 let url = baseServerURL + 'api/fetcherMini.jsp'; 1472 let payload = {input: text,pretext: '',posttext: '', action: 'native'}; 1473 let response = await fetch(url, {method: 'POST',headers: {'Content-Type': 'application/json'},body: JSON.stringify(payload)}); 1474 let responseText = await response.text(); 1475 hideX($(".snippetOptions .spinner")); 1476 responseText=toHTML(responseText.trim(),true); 1477 return responseText; 1478} 1479</script>
1479 1480 1481 1482<div class="invisible assets"> 1483<div class="countdown"><span class="number">3</span><span class="number">2</span><span class="number">1</span></div> 1484 1485<div class="loading-visuals"> 1486<div class="dot-container"><div class="dot"></div><div class="dot"></div><div class="dot"></div></div> 1487</div> 1488 1489<svg class="CloseSVG" width="50px" height="50px" viewBox="0 0 201 201" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> 1490 <title>close</title> 1491 <g class="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"><path class="shape-close" fill="#212427" fill-rule="nonzero" d="M100.683765,0.285933465 C45.3504317,0.285933465 0.683765055,44.9526001 0.683765055,100.285933 C0.683765055,155.619267 45.3504317,200.285933 100.683765,200.285933 C156.017098,200.285933 200.683765,155.619267 200.683765,100.285933 C200.683765,44.9526001 156.017098,0.285933465 100.683765,0.285933465 Z M151.572654,134.063711 C156.240114,138.731172 156.240114,146.28633 151.572654,150.952203 C149.12813,153.396727 146.238924,154.508116 143.127813,154.508116 C140.016781,154.508116 136.90559,153.396687 134.682971,150.952203 L101.351225,117.61887 L67.7960666,150.952203 C65.3515428,153.396727 62.4623365,154.508116 59.3512254,154.508116 C56.2401143,154.508116 53.1290032,153.396687 50.9063841,150.952203 C46.2389238,146.284743 46.2389238,138.729584 50.9063841,134.063711 L84.4615428,100.508553 L50.9063841,67.1736319 C46.2389238,62.5061716 46.2389238,54.9510128 50.9063841,50.2851398 C55.5738444,45.6176795 63.1290032,45.6176795 67.7948762,50.2851398 L101.350035,83.8402985 L134.905194,50.2851398 C139.572654,45.6176795 147.127813,45.6176795 151.793686,50.2851398 C156.461146,54.9526001 156.461146,62.5077589 151.793686,67.1736319 L118.240114,100.729981 L151.572654,134.063711 Z" ></path></g> 1492</svg> 1493 1494<svg class="RestartSVG" width="50px" height="50px" viewBox="0 0 201 201" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> 1495 <title>Restart</title> 1496 <g class="g-restart" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> 1497 <path class="shape-restart" d="M100,0.762600046 C73.4792818,0.762600046 48.0422985,11.2994593 29.2903999,30.053 C10.5385013,48.8065406 0,74.2398293 0,100.7626 C0,127.285371 10.5368593,152.720302 29.2903999,171.4722 C48.0439405,190.224099 73.4772293,200.7626 100,200.7626 C126.522771,200.7626 151.95
14977701,190.225741 170.7096,171.4722 C189.461499,152.71866 200,127.285371 200,100.7626 C200,74.2398293 189.463141,48.8048985 170.7096,30.053 C151.956059,11.3011014 126.522771,0.762600046 100,0.762600046 L100,0.762600046 Z M159.400169,116.369204 L119.217002,88.1165627 L139.240236,81.4507717 C133.857275,70.6187589 124.210392,62.5129141 112.613607,59.0782132 C101.01518,55.6434303 88.5124096,57.1876294 78.0991125,63.3404935 C67.6841734,69.4947944 60.2999975,79.7028386 57.7150434,91.5180126 C55.1301303,103.334418 57.5755371,115.694743 64.467451,125.634728 C71.3594305,135.57348 82.0760913,142.199452 94.0480784,143.921523 C106.020066,145.643718 118.170212,142.308359 127.584338,134.713881 C129.986453,132.784833 133.237001,132.282908 136.108899,133.39739 C138.982439,134.513472 141.041535,137.07589 141.511153,140.122704 C141.980977,143.167835 140.791168,146.23226 138.389053,148.162868 C125.114942,158.803995 107.987414,163.393449 91.1726915,160.81301 C74.3563271,158.2329 59.3905222,148.720745 49.9168726,134.588677 C40.443223,120.456609 37.329127,103.000677 41.3315572,86.4650984 C45.3339874,69.9295199 56.0855412,55.8294714 70.9729395,47.5939068 C85.8603378,39.3583423 103.518239,37.740129 119.654806,43.1362259 C135.789731,48.5323228 148.92427,60.4447866 155.860995,75.9799628 L174.7812,69.7037417 L159.400169,116.369204 Z" fill="#212427" fill-rule="nonzero" transform="translate(100.000000, 100.762600) scale(-1, 1) translate(-100.000000, -100.762600) "></path> 1498 </g> 1499</svg> 1500 1501<svg class="MicSVG" width="50px" height="50px" viewBox="0 0 206 205" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> 1502 <title>mic</title> 1503 <g class="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"><path class="shape-mic" fill="#212427" fill-rule="nonzero" d="M103.046334,0 C75.9189527,0 49.9012319,10.7772904 30.720689,29.9591226 C11.5388569,49.1426738 0.762855755,75.1578161 0.762855755,102.284768 C0.762855755,129.41172 11.5388569,155.429871 30.720689,174.610413 C49.9042402,193.792245 75.9193825,204.568247 103.046334,204.568247 C130.173286,204.568247 156.191437,193.792245 175.37198,174.610413 C194.553812,155.426862 205.329813,129.41172 205.329813,102.284768 C205.329813,84.330353 200.604085,66.6913878 191.625803,51.142384 C182.64924,35.5933802 169.736003,22.6801431 154.187429,13.7040101 C138.638425,4.72572819 120.99946,0 103.045045,0 L103.046334,0 Z M81.9155047,58.2434395 C81.9155047,50.6906979 85.9445791,43.7104071 92.4869363,39.9331768 C99.0292935,36.1575795 107.089161,36.1575795 113.62937,39.9331768 C120.171727,43.7104501 124.200801,50.6906979 124.200801,58.2434395 L124.200801,99.6876803 C124.200801,107.240422 120.171727,114.220713 113.62937,117.997943 C107.088732,121.77354 99.0288638,121.77354 92.4869363,117.997943 C85.9445791,114.22067 81.9155047,107.240422 81.9155047,99.6876803 L81.9155047,58.2434395 Z M142.083015,99.6876803 C142.067906,109.198015 138.584446,118.377428 132.282158,125.502121 C125.98176,132.626814 117.297439,137.20814 107.859305,138.386564 L107.859305,157.640597 L128.291762,157.640597 C130.984516,157.640597 133.16705,159.823001 133.16705,162.514165 C133.16705,165.205329 130.984645,167.387733 128.291762,167.387733 L77.7510534,167.387733 C75.0616542,167.387733 72.8791638,165.205329 72.8791638,162.514165 C72.8791638,159.823001 75.0615683,157.640597 77.7527321,157.640597 L98.2337527,157.640597 L98.2337527,138.386564 C88.7956191,137.208054 80.1112985,132.626814 73.8109006,125.502121 C67.5087837,118.377428 64.0250848,109.197585 64.0100429,99.6876803 L64.0100429,77.2323059 C64.0100429,74.541228 66.1924475,72.3587375 68.8836113,72.3587375 C71.5746891,72.3587375 73.7571796,74.5411421 73.7571796,77.2323059 L73.7571796,99.7113176 C73.7571796,110.166712 79.3355674,119.828325 88.3894884,125.056022 C97.4446988,130.283719 108.600185,130.283719 117.655825,125.056022 C126.711035,119.828325 132.288134,110.166712 132.288134,99.7113176 L132.288134,77.2323059 C132.288134,74.541228 134.470538,72.3587375 137.161702,72.3587375 C139.854456,72.3587375 142.035271,74.5411421 142.035271,77.2323059 L142.083015,99.6876803 Z" ></path></g> 1504</svg> 1505 1506<svg class="PauseSVG" width="50px" height="50px" viewBox="0 0 201 201" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> 1507 <title>pause</title> 1508 <g class="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"><path class="shape-pause" fill="#212427" fill-rule="nonzero" d="M171.155386,30.2412184 C189.249591,48.3370606 200.44414,73.3374699 200.44414,100.952464 C200.44414,128.567458 189.250819,153.567458 171.155386,171.66371 C153.057907,189.757915 128.055042,200.952464 100.44414,200.952464 C72.8332385,200.952464 47.8291462,189.759143 29.7328947,171.66371 C11.6386895,153.566231 0.444140435,128.563366 0.444140435,100.952464 C0.444140435,73.3415622 11.6374618,48.3374699 29.7345317,30.2412184 C47.8303739,12.1470132 72.8307831,0.950827218 100.445777,0.950827218 C128.05627,0.950827218 153.059135,12.1441485 171.155386,30.2412184 Z M71.9607553,56.5041093 C69.3295145,56.5041093 67.1985192,58.6365777 67.1985192,61.2663453 L67.1985192,140.628761 C67.1985192,143.260002 69.3309877,145.390997 71.9607553,145.390997 L88.8735069,145.390997 C91.5047477,145.390997 93.635743,143.258529 93.635743,140.628761 L93.635743,61.2663453 C93.635743,58.6351045 91.5032745,56.5041093 88.8735069,56.5041093 L71.9607553,56.5041093 Z M112.012728,56.5041093 C109.381487,56.5041093 107.250492,58.6365777 107.250492,61.2663453 L107.250492,140.628761 C107.250492,143.260002 109.38296,145.390997 112.012728,145.390997 L128.925479,145.390997 C131.55672,145.390997 133.687715,143.258529 133.687715,140.628761 L133.687715,61.2663453 C133.687715,58.6351045 131.555247,56.5041093 128.925479,56.5041093 L112.012728,56.5041093 Z" ></path></g> 1509</svg> 1510 1511 1512<svg class="SubmitSVG" width="50px" height="50px" viewBox="0 0 201 201" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> 1513 <title>submit</title> 1514 <g class="base" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> 1515 <g class="shape-submit" transform="translate(0.294576, 0.319687)" fill="#212427" fill-rule="nonzero"> 1516 <path d="M145.921053,52.6315789 C145.730147,52.6330474 145.540714,52.6712293 145.363008,52.741718 L37.2915789,92.9334474 C37.0272481,93.0303684 36.8495489,93.2800188 36.8435345,93.560515 C36.8378102,93.8424699 37.0052218,94.0979962 37.2651504,94.2066805 L80.723797,112.169462 L113.655376,79.2378835 C114.636353,78.2231466 115.98297,77.6430714 117.395677,77.6313421 C118.806917,77.619594 120.163835,78.174688 121.162594,79.173297 C122.161165,80.1704023 122.716278,81.5287857 122.704549,82.9402143 C122.691332,84.3514549 122.112744,85.6995376 121.098008,86.680515 L88.0021429,119.780891 L102.433346,162.247056 L102.434814,162.247056 C102.500897,162.534883 102.766698,162.730214 103.060416,162.70965 C103.724175,162.70965 103.655153,162.436508 103.718311,162.277921 L147.248386,54.6688985 L147.249855,54.6688985 C147.424606,54.2195376 147.373208,53.7143496 147.108877,53.3090489 C146.846012,52.9052143 146.40399,52.6540865 145.922336,52.6315789 L145.921053,52.6315789 Z" class="Path1"></path>
1517 <path class="path2" d="M100,0 C73.4785714,0 48.0413534,10.5349624 29.2894737,29.2894737 C10.5349624,48.0424812 0,73.4774436 0,100 C0,126.522556 10.5349624,151.958647 29.2894737,170.710526 C48.0424812,189.465038 73.4774436,200 100,200 C126.522556,200 151.958647,189.465038 170.710526,170.710526 C189.465038,151.957519 200,126.522556 200,100 C199.97063,73.487594 189.425188,48.0676692 170.678195,29.3233083 C151.931203,10.5763158 126.512782,0.0308270677 100.001504,0 L100,0 Z M157,58.6165414 L113.473684,166.225564 C112.64985,168.303496 111.218045,170.083459 109.366165,171.333083 C107.514361,172.584248 105.329323,173.248045 103.093985,173.236432 L102.762102,173.236432 L102.76357,173.236432 C100.472668,173.183414 98.2552992,172.425677 96.4109383,171.065827 C94.5679684,169.705977 93.1890211,167.810113 92.4635699,165.63688 L77.7271038,122.32485 L33.2421414,103.936128 C30.4490211,102.780414 28.2417654,100.545338 27.1199609,97.7406391 C25.9965398,94.9343233 26.0538331,91.7947744 27.2770925,89.0323684 C28.5018293,86.268609 30.7882955,84.1158271 33.6210774,83.0628195 L141.700025,42.8710902 C144.623822,41.7843985 147.85604,41.8783835 150.710927,43.1339549 C153.565739,44.3910226 155.81995,46.709782 156.994762,49.5982406 C158.168108,52.4882782 158.17104,55.7219248 157.000636,58.6132782 L157,58.6165414 Z" ></path> 1518 </g> 1519 </g> 1520</svg> 1521 1522<svg class="ResetSVG" width="50px" height="50px" viewBox="0 0 200 201" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> 1523 <title>reset</title> 1524 <g class="icons" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"><path d="M100,0.762600046 C73.4792818,0.762600046 48.0422985,11.2994593 29.2903999,30.053 C10.5385013,48.8065406 0,74.2398293 0,100.7626 C0,127.285371 10.5368593,152.720302 29.2903999,171.4722 C48.0439405,190.224099 73.4772293,200.7626 100,200.7626 C126.522771,200.7626 151.95
15247701,190.225741 170.7096,171.4722 C189.461499,152.71866 200,127.285371 200,100.7626 C200,74.2398293 189.463141,48.8048985 170.7096,30.053 C151.956059,11.3011014 126.522771,0.762600046 100,0.762600046 L100,0.762600046 Z M159.400169,116.369204 L119.217002,88.1165627 L139.240236,81.4507717 C133.857275,70.6187589 124.210392,62.5129141 112.613607,59.0782132 C101.01518,55.6434303 88.5124096,57.1876294 78.0991125,63.3404935 C67.6841734,69.4947944 60.2999975,79.7028386 57.7150434,91.5180126 C55.1301303,103.334418 57.5755371,115.694743 64.467451,125.634728 C71.3594305,135.57348 82.0760913,142.199452 94.0480784,143.921523 C106.020066,145.643718 118.170212,142.308359 127.584338,134.713881 C129.986453,132.784833 133.237001,132.282908 136.108899,133.39739 C138.982439,134.513472 141.041535,137.07589 141.511153,140.122704 C141.980977,143.167835 140.791168,146.23226 138.389053,148.162868 C125.114942,158.803995 107.987414,163.393449 91.1726915,160.81301 C74.3563271,158.2329 59.3905222,148.720745 49.9168726,134.588677 C40.443223,120.456609 37.329127,103.000677 41.3315572,86.4650984 C45.3339874,69.9295199 56.0855412,55.8294714 70.9729395,47.5939068 C85.8603378,39.3583423 103.518239,37.740129 119.654806,43.1362259 C135.789731,48.5323228 148.92427,60.4447866 155.860995,75.9799628 L174.7812,69.7037417 L159.400169,116.369204 Z" class="shape-reset" fill="#212427" fill-rule="nonzero" transform="translate(100.000000, 100.762600) scale(-1, 1) translate(-100.000000, -100.762600) "></path></g> 1525</svg> 1526 1527<svg class="DoneSVG" width="50px" height="50px" viewBox="0 0 200 200" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> 1528 <title>done-bg</title> 1529 <g class="base" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> 1530 <g class="shape-submit" fill="#212427" fill-rule="nonzero"> 1531 <path d="M100,0 C73.4783354,0 48.0435435,10.5370077 29.2889898,29.2907042 C10.5352934,48.0444006 0,73.4813354 0,100.001714 C0,126.522093 10.5352934,151.958171 29.2889898,170.712724 C48.0426863,189.466421 73.4796211,200.001714 100,200.001714 C126.520379,200.001714 151.956457,189.466421 170.71101,170.712724 C189.464707,151.959028 200,126.522093 200,100.001714 C200,82.4484635 195.379505,65.20336 186.603523,49.9995714 C177.826255,34.7983543 165.201217,22.1737453 150.000429,13.3964771 C134.797497,4.62049458 117.552394,0 99.9982857,0 L100,0 Z M148.30069,75.5042215 L108.30069,135.505079 L108.30069,135.503404 C106.832469,137.724979 104.540608,139.268544 101.932456,139.794323 C99.3241761,140.318344 96.6138088,139.782604 94.4006343,138.30433 L64.4002057,118.303473 C61.649595,116.235929 60.1563022,112.899538 60.4458921,109.470918 C60.7354819,106.042298 62.7679253,103.001968 65.8266832,101.425232 C68.8836412,99.8481948 72.5382077,99.9536678 75.4996786,101.703138 L97.1998457,116.202774 L131.700339,64.5034642 L131.700339,64.5051386 C133.769554,61.7545279 137.105987,60.2612352 140.534608,60.550825 C143.963228,60.8404491 147.001843,62.8728582 148.580294,65.9299019 C150.157331,68.9885313 150.051858,72.6431406 148.300716,75.6046115 L148.30069,75.5042215 Z" id="Shape"></path> 1532 </g> 1533 </g> 1534</svg> 1535 1536</div> 1537 1538<style> 1539 .main-box.talk-page .area-buttons-talk .button-set1 .button-submit-talk {position:relative;} 1540 /* the one-time hint: same ink pill as the site's tooltips (tabbiTips.inc) */ 1541 .main-box.talk-page .tf-hint {position:absolute; left:50%; bottom:calc(100% + 8px); transform:translateX(-50%); background:#211d1e; color:#fff; 1542 font-size:12px; line-height:1; font-weight:500; padding:6px 9px; border-radius:6px; white-space:nowrap; pointer-events:none; opacity:0; 1543 transition:opacity .2s ease; z-index:60;} 1544 .main-box.talk-page .tf-hint.on {opacity:1;} 1545 .main-box.talk-page .area-buttons-talk .button-submit-talk.tf-busy {opacity:.55; pointer-events:none;} 1546 .main-box.talk-page .tf-note {color:#bf3143;} 1547 /* 3-2-1 countdown replaced by the real state */ 1548 .main-box.talk-page .countdown-animation {display:none !important;} 1549 .main-box.talk-page .button-big-mic.tf-connecting img {animation:tfPulse 1s ease-in-out infinite;} 1550 @keyframes tfPulse {0%,100% {transform:scale(1); opacity:1;} 50% {transform:scale(.93); opacity:.55;}}
1550 1551 .main-box.talk-page .talk-mode:has(.area-talk-input.recording-on) .status-message::before {content:'Listening \00B7 '; font-weight:600; color:#bf3143; 1552 padding-left:16px; background:radial-gradient(circle at 5px 50%, #bf3143 4.5px, transparent 5.5px) no-repeat; animation:tfBlink 1.2s ease-in-out infinite;} 1553 @keyframes tfBlink {0%,100% {opacity:1;} 50% {opacity:.45;}} 1554</style>
1555<script> 1556(function(){ 1557 if (!document.querySelector('.main-box.talk-page')) {return;} 1558 const sleep=(ms)=>new Promise(r=>setTimeout(r,ms)); 1559 const q=(s)=>document.querySelector('.area-audiogroup '+s); 1560 const isRecording=()=>{const t=q('.button-toggle-voice'); return !!(t && t.classList.contains('voiceIsOn'));}; 1561 const spokenText=()=>{const a=q('.js-area-input'); return a ? a.innerText.replace(/\s+/g,' ').trim() : '';}; 1562 1563 // Fix 2: use the name the server really saved the recording under. 1564 window.__tfUpload=null; 1565 // Fix 5 (April 2026-09-29): pausing and resuming makes one audio piece per segment, and the page gave the play button (and the saved 1566 // copy) only the newest piece. With 2+ pieces we join them in order into one 16 kHz WAV and use that for both. One piece = unchanged. 1567 const parts=[]; 1568 async function doUpload(blob, name){ // true once the server has it; only then does the chat point at it 1569 const fd=new FormData(); fd.append('file', new File([blob], name)); fd.append('filename', name); 1570 const r=await fetch(baseServerURL+'api/fileUpload.jsp', {method:'POST', body:fd}); 1571 const d=r.ok ? await r.json() : null; 1572 if (!d || d.success!=='true') {console.error('audio upload refused: '+name+' ('+r.status+')'); return false;} 1573 latestPlaybackId=d.savedAs || name; // guests: scrambled name (fix 2) 1574 console.log('audioFile uploaded: '+latestPlaybackId); 1575 return true; 1576 } 1577 function wavFrom(buf){ 1578 const n=buf.length, rate=buf.sampleRate, data=buf.getChannelData(0), out=new DataView(new ArrayBuffer(44+n*2)); 1579 const w=(o,t)=>{for (let i=0;i<t.length;i++) {out.setUint8(o+i, t.charCodeAt(i));}}; 1580 w(0,'RIFF'); out.setUint32(4,36+n*2,true); w(8,'WAVE'); w(12,'fmt '); out.setUint32(16,16,true); out.setUint16(20,1,true); out.setUint16(22,1,true); 1581 out.setUint32(24,rate,true); out.setUint32(28,rate*2,true); out.setUint16(32,2,true); out.setUint16(34,16,true); w(36,'data'); out.setUint32(40,n*2,true); 1582 for (let i=0,o=44;i<n;i++,o+=2) {const v=Math.max(-1,Math.min(1,data[i])); out.setInt16(o, v<0 ? v*0x8000 : v*0x7FFF, true);} 1583 return new Blob([out], {type:'audio/wav'}); 1584 } 1585 async function joinParts(blobs){ 1586 const AC=window.AudioContext||window.webkitAudioContext, OAC=window.OfflineAudioContext||window.webkitOfflineAudioContext; 1587 const ctx=new AC(); 1588 try { 1589 const bufs=[]; 1590 for (const b of blobs) {const ab=await b.arrayBuffer(); bufs.push(await new Promise((res,rej)=>ctx.decodeAudioData(ab,res,rej)));} 1591 const rate=16000, total=bufs.reduce((n,b)=>n+Math.ceil(b.duration*rate),0); 1592 const off=new OAC(1,total,rate); let t=0; 1593 for (const b of bufs) {const src=off.createBufferSource(); src.buffer=b; src.connect(off.destination); src.start(t); t+=b.duration;} 1594 return wavFrom(await off.startRendering()); 1595 } finally {try {ctx.close();} catch (e) {}} 1596 } 1597 // Each piece is uploaded as before (small, always playable), so the chat never points at a file that isn't there. With 2+ pieces 1598 // the joined copy follows and takes over only once the server has it (the web server refuses uploads over 1 MB, about 30 s of WAV; 1599 // a refused or slow joined copy leaves the chat on the last piece, never on a missing file - April's 17:03 talk, 2026-09-29). 1600 window.uploadAudioFile=function(audioBlob, filename){ 1601 if (audioBlob && audioBlob.size>0) {parts.push(audioBlob);} 1602 window.__tfUpload=(async function(){
1603 try { 1604 const saved=await doUpload(audioBlob, filename); 1605 if (!saved && latestPlaybackId===filename) {latestPlaybackId='';} // nothing to play rather than a broken play button 1606 if (parts.length<2) {return;} 1607 let joined=null; 1608 try {joined=await joinParts(parts);} catch (e) {console.error('joining the recording failed; keeping the last piece', e);} 1609 if (!joined) {return;} 1610 const pp=document.getElementById('playbackPlayer'); 1611 if (pp) {pp.src=URL.createObjectURL(joined); try {pp.playbackRate=FF_defaultAudioPlaybackRate;} catch (e) {}} 1612 await doUpload(joined, filename.replace(/\.[A-Za-z0-9]+$/,'')+'-all.wav'); 1613 } catch (e) {console.error('audio upload failed', e);} 1614 })(); 1615 return window.__tfUpload; 1616 }; 1617 document.addEventListener('click', function(ev){ // a new talk starts from nothing 1618 if (ev.target && ev.target.closest && ev.target.closest('.area-audiogroup .button-reset-text, .area-audiogroup .button-restart-text')) {parts.length=0;} 1619 }, true); 1620 1621 // Fix 7 (April 2026-09-29: "when there is a longer pause on talking, I want to see space"): a silence of 2 s or more between two 1622 // stretches of speech starts a new paragraph (a blank line: a single line break looked like ordinary wrapping). The transcription service reports when each word was spoken; the 1623 // page joins its turns with a space, so the turn that follows a long silence gets a leading newline before the page sees it. 1624 // (The page's script is versioned and shared, so this wraps the socket instead of editing it.) 1625 const GAP_MS=2000; 1626 let resumedJustNow=false; // the first words after pause/resume already start on a new line 1627 (function(){ 1628 const NativeWS=window.WebSocket; if (!NativeWS) {return;} 1629 function TfWS(url, protocols){ 1630 const ws=(protocols===undefined) ? new NativeWS(url) : new NativeWS(url, protocols); 1631 if (String(url).indexOf('streaming.assemblyai.com')<0) {return ws;} 1632 const span={}, lineBreak={}; let handler=null; 1633 Object.defineProperty(ws, 'onmessage', {configurable:true, get:function(){return handler;}, set:function(fn){handler=fn;}}); 1634 ws.addEventListener('message', function(ev){ 1635 if (!handler) {return;} 1636 let out=ev.data; 1637 try { 1638 const d=JSON.parse(ev.data); 1639 const w=d && d.type==='Turn' && Array.isArray(d.words) ? d.words : null; 1640 if (w && w.length) { 1641 const n=d.turn_order||0; 1642 span[n]={start:w[0].start, end:w[w.length-1].end}; 1643 if (lineBreak[n]===undefined) { 1644 const prev=span[n-1]; 1645 lineBreak[n]=!resumedJustNow && !!prev && (w[0].start-prev.end)>=GAP_MS; 1646 resumedJustNow=false; 1647 } 1648 if (lineBreak[n] && d.transcript) {d.transcript='\n\n'+d.transcript; out=JSON.stringify(d);} 1649 } 1650 } catch (e) {} 1651 handler.call(ws, {data:out}); 1652 }); 1653 return ws; 1654 } 1655 TfWS.prototype=NativeWS.prototype; 1656 ['CONNECTING','OPEN','CLOSING','CLOSED'].forEach(function(k){TfWS[k]=NativeWS[k];}); 1657 window.WebSocket=TfWS; 1658 })(); 1659 document.addEventListener('click', function(ev){ 1660 if (ev.target && ev.target.closest && ev.target.closest('.area-audiogroup .button-toggle-voice') && !isRecording()) {resumedJustNow=true;} 1661 }, true); 1662 1663 // The empty box holds the markup's line breaks, and each segment starts with a newline: trim blank space before the first words. 1664 let trimming=false; 1665 function trimLeadingBlank(){ 1666 const a=q('.talk-mode .js-area-input'); if (!a || trimming) {return;} 1667 let n=a.firstChild; trimming=true; 1668 try { 1669 while (n) { 1670 if (n.nodeType===3) { 1671 const t=n.nodeValue.replace(/^[\s\u00a0]+/,''); 1672 if (t.length) {if (t!==n.nodeValue) {n.nodeValue=t;} break;} 1673 const next=n.nextSibling; a.removeChild(n); n=next; continue; 1674 } 1675 if (n.nodeType===1 && n.classList.contains('cursor-talk-input')) {n=n.nextSibling; continue;} 1676 if (n.nodeName==='BR') {const next=n.nextSibling; a.removeChild(n); n=next; continue;} 1677 break; 1678 } 1679 } finally {trimming=false;} 1680 } 1681 function note(msg){ 1682 const s=q('.status-message'); if (!s) {return;} 1683 s.innerHTML='<span class="tf-note">'+msg+'</span>'; 1684 } 1685 function captions(){ // tooltip texts (shown by tabbiTips on hover-capable screens) 1686 const reset=document.querySelectorAll('.area-audiogroup .button-reset-text, .area-audiogroup .button-restart-text'); 1687 reset.forEach(b=>b.setAttribute('data-tip','Start over')); 1688 document.querySelectorAll('.area-audiogroup .button-submit-talk').forEach(b=>b.setAttribute('data-tip','Get feedback')); 1689 const t=q('.button-toggle-voice'); 1690 if (t) {t.setAttribute('data-tip', t.classList.contains('timeOver') ? "Time's up" : isRecording() ? 'Pause' : (spokenText() ? 'Resume' : 'Record'));} 1691 } 1692 // The one-time hint for the key step (phones included). 1693 const HINT_KEY='tabbiTalkHint1', HINT_TIMES=3; 1694 let hintTimer=null, wasRecording=false; 1695 function hintCount(){try {return parseInt(localStorage.getItem(HINT_KEY)||'0',10)||0;} catch (e) {return HINT_TIMES;}} 1696 function hideHint(){clearTimeout(hintTimer); const h=q('.tf-hint'); if (h) {h.classList.remove('on');}} 1697 function onRecordingChange(){ 1698 const now=isRecording(); 1699 if (now) {const big=q('.button-big-mic'); if (big) {big.classList.remove('tf-connecting');}} 1700 if (now && !wasRecording && hintCount()<HINT_TIMES) { 1701 clearTimeout(hintTimer); 1702 hintTimer=setTimeout(function(){ 1703 const s=q('.button-set1 .button-submit-talk'); if (!s || !isRecording()) {return;} 1704 let h=s.querySelector('.tf-hint'); if (!h) {h=document.createElement('span'); h.className='tf-hint'; h.textContent='Tap when you\u2019re done'; s.appendChild(h);} 1705 h.classList.add('on');
1706 try {localStorage.setItem(HINT_KEY, String(hintCount()+1));} catch (e) {} 1707 }, 3500); 1708 } 1709 if (!now) {hideHint();} 1710 wasRecording=now; 1711 } 1712 // Fix 1a: "Get feedback" stays usable while recording. 1713 function keepSubmitUsable(){ 1714 const s=q('.button-set1 .button-submit-talk'); 1715 if (s && isRecording() && !s.classList.contains('js-active')) {s.classList.add('js-active');} 1716 } 1717 1718 // Fix 1b: one step from speaking to feedback. 1719 let passThrough=false; 1720 document.addEventListener('click', async function(ev){ 1721 const b=ev.target && ev.target.closest && ev.target.closest('.area-audiogroup .button-submit-talk'); 1722 if (!b || passThrough) {return;} 1723 if (!isRecording() && spokenText() && !window.__tfUpload) {return;} // normal case: let the page send it 1724 ev.preventDefault(); ev.stopImmediatePropagation(); 1725 b.classList.add('tf-busy'); 1726 try { 1727 if (isRecording()) { 1728 $(q('.button-toggle-voice')).trigger('click'); // stop; the page waits for the last words 1729 const t0=Date.now(); 1730 while (Date.now()-t0<8000 && (isRecording() || !b.classList.contains('js-active'))) {await sleep(120);} 1731 const t1=Date.now(); // the recording's upload starts just after the stop 1732 while (!window.__tfUpload && Date.now()-t1<2000) {await sleep(100);} 1733 } 1734 if (!spokenText()) {note("We didn\u2019t catch any words. Tap Record and try again, a little closer to the mic."); return;} 1735 if (window.__tfUpload) {await Promise.race([window.__tfUpload, sleep(10000)]); window.__tfUpload=null;} 1736 passThrough=true; b.classList.remove('tf-busy'); b.click(); 1737 } finally {passThrough=false; b.classList.remove('tf-busy');} 1738 }, true); 1739 1740 $(function(){ 1741 setTimeout(function(){ 1742 captions(); 1743 const t=q('.button-toggle-voice'), s=q('.button-set1 .button-submit-talk'), a=q('.js-area-input'); 1744 const mo=new MutationObserver(function(){keepSubmitUsable(); captions(); onRecordingChange();}); 1745 const sb=q('.button-set1 .button-submit-talk'); if (sb) {sb.addEventListener('mouseenter', hideHint); sb.addEventListener('click', hideHint, true);} 1746 if (t) {mo.observe(t, {attributes:true, attributeFilter:['class']});} 1747 if (s) {mo.observe(s, {attributes:true, attributeFilter:['class']});} 1748 if (a) {new MutationObserver(function(){trimLeadingBlank(); captions();}).observe(a, {childList:true, subtree:true, characterData:true});} 1749 // (1) no browser tooltips from the icons' built-in SVG titles 1750 document.querySelectorAll('.area-audiogroup .button-state svg title').forEach(function(t){t.remove();}); 1751 // (3) tapping the mic: pulse + "Getting the mic ready..." until recording truly starts 1752 document.querySelectorAll('.area-audiogroup .button-big-mic, .area-audiogroup .button-toggle-voice').forEach(function(m){ 1753 m.addEventListener('click', function(){ 1754 if (isRecording()) {return;} 1755 const big=q('.button-big-mic'); if (big) {big.classList.add('tf-connecting');} 1756 setTimeout(function(){ if (!isRecording()) {const st=q('.status-message'); if (st && /connecting/i.test(st.textContent)) {st.textContent='Getting the mic ready\u2026';}} }, 0); 1757 }, true); 1758 }); 1759 }, 0); 1760 }); 1761})(); 1762</script>
1762 1763 1764 1765<style> 1766 .tf-replay {display:inline-flex; cursor:pointer;} .tf-replay svg {width:26px; height:26px; display:block;} 1767</style>
1768<script> 1769(function(){ 1770 const REPLAY='<svg viewBox="0 0 26 26" fill="none" stroke="#211d1e" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M5.2 9.2A8.6 8.6 0 1 1 4.6 15"/><path d="M4.6 4.8v4.6h4.6"/></svg>'; 1771 const PAUSE_IMG='images/pause-line-black.png'; 1772 function player(){return document.getElementById('playbackPlayer');} 1773 function fileOf(el){ // History: the chat's saved recording; Talk: none (the page loads the player itself) 1774 const c=el && el.closest && el.closest('.hasPlayback'); 1775 const id=c && c.getAttribute('playbackId'); 1776 return id ? '/audex/'+id : null; 1777 } 1778 function isLoaded(el,p){const f=fileOf(el); return !f || p.dataset.tfFile===f;} 1779 function start(p){ 1780 try {stopRealAloud();} catch (e) {} 1781 try {p.playbackRate=FF_defaultAudioPlaybackRate;} catch (e) {} 1782 const r=p.play(); if (r && r.catch) {r.catch(function(){});} 1783 } 1784 function load(el,p){ // point the player at this chat's recording, if it isn't already 1785 const f=fileOf(el); if (!f || p.dataset.tfFile===f) {return;} 1786 p.pause(); p.src=f; p.dataset.tfFile=f; p.load(); 1787 } 1788 let queued=false; 1789 function renderPlayback(){ 1790 const p=player(); if (!p) {return;} 1791 const active=!p.paused && !p.ended; 1792 try {playbackOn=active;} catch (e) {} 1793 document.querySelectorAll('.button-audio-playback').forEach(function(b){ 1794 const mine=isLoaded(b,p); 1795 const playing=mine && active, started=mine && p.currentTime>0.05 && !p.ended; 1796 let pz=b.querySelector('.tf-pause'); 1797 if (!pz) {pz=document.createElement('img'); pz.className='icon-multi tf-pause'; pz.src=PAUSE_IMG; pz.alt='Pause'; pz.style.display='none'; b.appendChild(pz);} 1798 b.querySelectorAll('.image-two, .image-three').forEach(function(i){if (i.style.display!=='none') {i.style.display='none';}}); 1799 const one=b.querySelector('.image-one'); if (one) {one.style.display=playing?'none':'';} 1800 pz.style.display=playing?'':'none'; 1801 const combo=b.closest('.mic-play-combo'); 1802 if (combo) { 1803 let r=combo.querySelector('.tf-replay-div'); 1804 if (!r) { 1805 r=document.createElement('div'); r.className='clickable tf-replay-div invisible'; 1806 r.innerHTML='<span class="tf-replay" data-tip="Play from the start" role="button" aria-label="Play from the start">'+REPLAY+'</span>'; 1807 combo.appendChild(r); 1808 } 1809 const hide=!(started || playing); 1810 if (r.classList.contains('invisible')!==hide) {r.classList.toggle('invisible', hide);} 1811 } 1812 }); 1813 } 1814 function later(){ if (queued) {return;} queued=true; setTimeout(function(){queued=false; renderPlayback();},0); } 1815 // Result cards are copies of a hidden template, and copies don't keep element listeners: listen at the document instead. 1816 document.addEventListener('click', function(ev){ 1817 const r=ev.target && ev.target.closest && ev.target.closest('.tf-replay-div'); if (!r) {return;} 1818 ev.stopPropagation(); const p=player(); if (!p) {return;} 1819 load(r,p); if (!p.src) {return;} 1820 p.currentTime=0; start(p); renderPlayback(); 1821 }); 1822 window.togglePlayback=function(xButton){ 1823 const p=player(); if (!p) {return false;} 1824 const b=xButton && xButton.jquery ? xButton[0] : xButton; 1825 if (b && !isLoaded(b,p)) {load(b,p); start(p); renderPlayback(); return false;} // another chat's recording 1826 load(b,p); if (!p.src) {return false;} 1827 if (p.paused || p.ended) {if (p.ended) {p.currentTime=0;} start(p);} 1828 else {p.pause();} 1829 renderPlayback(); return false; 1830 }; 1831 window.setPlaybackState=function(xButton,on){ // older callers (e.g. "stop playback" when read-aloud starts) 1832 const p=player(); if (!p) {return;} 1833 if (on) {if (p.paused || p.ended) {window.togglePlayback(xButton);}} 1834 else {p.pause();} 1835 renderPlayback(); 1836 }; 1837 const p=player(); if (!p) {return;} 1838 ['play','pause','ended','emptied','loadedmetadata'].forEach(function(ev){p.addEventListener(ev, later);}); 1839 p.addEventListener('ended', function(){p.currentTime=0; later();}); 1840 new MutationObserver(function(m){ if (m.some(function(x){return x.addedNodes && x.addedNodes.length;})) {later();} }) 1841 .observe(document.body, {childList:true, subtree:true}); 1842 $(function(){setTimeout(renderPlayback,0);}); 1843})(); 1844</script>
1844 1845
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.