1import { useState } from 'react'; 2import { html } from '../html.js?v=3.8.36'; 3import { Sparkles } from 'lucide-react'; 4import SemanticSearchHelpDialog from './SemanticSearchHelpDialog.js?v=3.8.36'; 5 6/** 7 * Short line for the archive's live region, announced when the semantic state 8 * changes (#279). 9 * 10 * The toggle is rendered twice, once beside each search box, so it must not 11 * carry the live region itself: two copies in the accessibility tree announce 12 * one change twice. App renders one live region and feeds it this message. 13 */ 14export function semanticStatusMessage({ enabled, status = 'idle', coverage = null } = {}) { 15 if (status === 'error') { 16 return 'Search by meaning is not available. Search by words still works.'; 17 } 18 if (!enabled) return 'Search by meaning is off.'; 19 if (status === 'loading') return 'Search by meaning is loading the language model.'; 20 if (status === 'searching') return 'Finding records by meaning.'; 21 return Number.isFinite(coverage) && coverage > 0 22 ? `Search by meaning is on. Searching ${coverage.toLocaleString()} records.` 23 : 'Search by meaning is on.'; 24} 25 26/** 27 * The opt-in semantic search switch that sits beside the archive search box 28 * (#279). 29 * 30 * Off by default, and off costs nothing: no model, no vectors, no extra bytes. 31 * Turning it on downloads a language model once, so the off state says so 32 * before the reader commits to it. 33 * 34 * The on state reports the loaded coverage so the reader can confirm how much 35 * of the archive is ready to search by meaning. 36 * 37 * A plain checkbox, not a custom widget: it is keyboard operable, announces its 38 * own state, and takes the app's focus ring for free. `describedBy` ties the 39 * hint to the control so a screen reader hears the cost and the current state 40 * with the label. The hint is a description only; the change announcement comes 41 * from the single live region App owns. 42 */ 43const SemanticSearchToggle = ({ 44 enabled, 45 status = 'idle', 46 coverage = null, 47 onToggle, 48 inputId = 'semantic-search', 49}) => { 50 const [helpOpen, setHelpOpen] = useState(false); 51 const hintId = `${inputId}-hint`; 52 const helpId = `${inputId}-help`; 53 const covered = Number.isFinite(coverage) && coverage > 0 54 ? coverage.toLocaleString() 55 : null; 56 57 let hint; 58 if (status === 'error') { 59 hint = 'Search by meaning is not available right now. Search by words still works.'; 60 } else if (!enabled) { 61 hint = 'Also find records that match what you mean, not just the words you typed. Downloads about 50 MB the first time.'; 62 } else if (status === 'loading') { 63 hint = 'Loading the language model. The first time takes a few seconds.'; 64 } else if (status === 'searching') { 65 hint = 'Finding records by meaning...'; 66 } else { 67 hint = covered 68 ? `Searching ${covered} records by meaning. Each result says how it matched.` 69 : 'Searching records by meaning. Each result says how it matched.'; 70 } 71 72 return html` 73 <div className="archive-semantic-toggle"> 74 <label className="archive-semantic-toggle__control" htmlFor=${inputId}> 75 <input 76 id=${inputId} 77 type="checkbox" 78 checked=${Boolean(enabled)} 79 onChange=${event => onToggle(event.target.checked)} 80 aria-describedby=${hintId} 81 /> 82 <${Sparkles} className="archive-semantic-toggle__icon" aria-hidden="true" /> 83 <span>Search by meaning</span> 84 </label> 85 <p id=${hintId} className="archive-semantic-toggle__hint"> 86 ${hint} 87 </p> 88 <button 89 type="button" 90 className="archive-semantic-toggle__help" 91 onClick=${() => setHelpOpen(true)} 92 aria-haspopup="dialog" 93 aria-controls=${helpId} 94 > 95 How does this work? 96 </button> 97 <${SemanticSearchHelpDialog} 98 isOpen=${helpOpen} 99 onClose=${() => setHelpOpen(false)} 100 coverage=${coverage} 101 dialogId=${helpId} 102 /> 103 </div> 104 `; 105}; 106 107export default SemanticSearchToggle;
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.