PageSourceSearch

https://vibe.nikolaisemenov.com/ftir/

html nikolaisemenov.com collected 2026-10-10 02:32:47 UTC 28,963 bytes, 401 lines download raw bytes

1<!doctype html>
2<html lang="en">
3<head>
4  <meta charset="UTF-8">
5  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6  <title>FTIR Merger</title>
7  <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@24,400,0,0" />
8  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css">
9  <link rel="stylesheet" href="styles.css">
10  
10<script src="./d3.min.js"></script>
10
11  
11<script src="./jcampconverter.min.js"></script>
11
12  
12<script src="https://unpkg.com/[email protected]/dist/smiles-drawer.min.js"></script>
vendor: 1 bytes, line 12
12
13<script data-cfasync="false" nonce="0ba5534b-86c4-40c1-837e-3ceb686da802">try{(function(w,d){!function(kP,kQ,kR,kS){if(kP.zaraz)console.error("zaraz is loaded twice");else{kP[kR]=kP[kR]||{};kP[kR].executed=[];kP.zaraz={deferred:[],listeners:[]};kP.zaraz._v="20";kP.zaraz._n="0ba5534b-86c4-40c1-837e-3ceb686da802";kP.zaraz.q=[];kP.zaraz._f=function(kT){return async function(){var kU=Array.prototype.slice.call(arguments);kP.zaraz.q.push({m:kT,a:kU})}};for(const kV of["track","set","debug"])kP.zaraz[kV]=kP.zaraz._f(kV);kP.zaraz.init=()=>{var kW=kQ.getElementsByTagName(kS)[0],kX=kQ.createElement(kS),kY=kQ.getElementsByTagName("title")[0];kY&&(kP[kR].t=kQ.getElementsByTagName("title")[0].text);kP[kR].x=Math.random();kP[kR].w=kP.screen.width;kP[kR].h=kP.screen.height;kP[kR].j=kP.innerHeight;kP[kR].e=kP.innerWidth;kP[kR].l=kP.location.href;kP[kR].r=kQ.referrer;kP[kR].k=kP.screen.colorDepth;kP[kR].n=kQ.characterSet;kP[kR].o=(new Date).getTimezoneOffset();if(kP.dataLayer)for(const kZ of Object.entries(Object.entries(dataLayer).reduce((k$,la)=>({...k$[1],...la[1]}),{})))zaraz.set(kZ[0],kZ[1],{scope:"page"});kP[kR].q=[];for(;kP.zaraz.q.length;){const lb=kP.zaraz.q.shift();kP[kR].q.push(lb)}kX.defer=!0;for(const lc of[localStorage,sessionStorage])Object.keys(lc||{}).filter(le=>le.startsWith("_zaraz_")).forEach(ld=>{try{kP[kR]["z_"+ld.slice(7)]=JSON.parse(lc.getItem(ld))}catch{kP[kR]["z_"+ld.slice(7)]=lc.getItem(ld)}});kX.referrerPolicy="origin";kX.src="/cdn-cgi/zaraz/s.js?z="+btoa(encodeURIComponent(JSON.stringify(kP[kR])));kW.parentNode.insertBefore(kX,kW)};["complete","interactive"].includes(kQ.readyState)?zaraz.init():kP.addEventListener("DOMContentLoaded",zaraz.init)}}(w,d,"zarazData","script");window.zaraz._p=async kH=>new Promise(kI=>{if(kH){kH.e&&kH.e.forEach(kJ=>{try{const kK=d.querySelector("script[nonce]"),kL=kK?.nonce||kK?.getAttribute("nonce"),kM=d.createElement("script");kL&&(kM.nonce=kL);kM.innerHTML=kJ;kM.onload=()=>{d.head.removeChild(kM)};d.head.appendChild(kM)}catch(kN){console.error(`Error executing script: ${kJ}\n`,kN)}});Promise.allSettled((kH.f||[]).map(kO=>fetch(kO[0],kO[1])))}kI()});zaraz._p({"e":["(function(w,d){})(window,document)"]});})(window,document)}catch(e){throw fetch("/cdn-cgi/zaraz/t"),e;};</script>
13</head>
14<body>
15  <nav class="tool-nav" aria-label="Multitool">
16    <a href="/">Multitool</a><a href="/search/">Search</a><a href="/qr/">QR</a><a href="/pdf/">PDF</a><a href="/badge/">Badge</a><a href="/ftir/" aria-current="page">FTIR</a>
17  </nav>
18  <div class="bg-accent"></div>
19  <div class="app-shell container-fluid px-1">
20    <header class="topbar surface">
21      <div class="brand">
22        <img src="logo.png" alt="FTIR Merger logo" class="brand-logo" />
23        <div class="brand-copy">
24          <p class="eyebrow">Spectra workspace</p>
25          <strong>FTIR viewer</strong>
26        </div>
27      </div>
28      <div class="topbar-form toolbar">
29        <input id="files" type="file" accept=".txt,.jdx,.dx,.jsm,.jcm,.csv,text/plain,application/octet-stream" multiple style="display:none;">
30        <input id="importSessionInput" type="file" accept=".json" style="display:none;">
31        <input id="importUserSettingsInput" type="file" accept=".json,application/json" style="display:none;">
32        <div class="toolbar-group">
33          <button id="selectFiles" class="icon-btn primary" type="button" title="Add files" aria-label="Add files">
34            <span class="material-symbols-outlined">computer_arrow_up</span>
35          </button>
36          <button id="exportSession" class="icon-btn" type="button" title="Export session" aria-label="Export session">
37            <span class="material-symbols-outlined">file_download</span>
38          </button>
39          <button id="importSession" class="icon-btn" type="button" title="Import session" aria-label="Import session">
40            <span class="material-symbols-outlined">unarchive</span>
41          </button>
42          <button id="clearLocalSession" class="icon-btn" type="button" title="Clear local session" aria-label="Clear local session">
43            <span class="material-symbols-outlined">delete_sweep</span>
44          </button>
45          <button id="copyPng" class="icon-btn saveimg" type="button" title="Copy chart PNG" aria-label="Copy chart PNG">
46            <span class="material-symbols-outlined">image</span>
47          </button>
48          <button id="copySvg" class="icon-btn saveimg" type="button" title="Copy chart SVG" aria-label="Copy chart SVG">
49            <span class="material-symbols-outlined">lasso_select</span>
50          </button>
51          <button id="saveCsv" class="icon-btn success" type="button" title="Save CSV" aria-label="Save CSV">
52            <span class="material-symbols-outlined">save</span>
53          </button>
54          <button id="openAppSettings" class="icon-btn" type="button" title="Application settings" aria-label="Application settings">
55            <span class="material-symbols-outlined">settings</span>
56          </button>
57        </div>
58        <div class="toolbar-group name-group">
59          <label class="visually-hidden" for="fileName">CSV name</label>
60          <input id="fileName" type="text" class="save-name" value="merged.csv" aria-label="CSV file name">
61        </div>
62      </div>
63      <div class="lang-switch">
64        <a href="#" class="lang-link" data-lang="en">EN</a>
65        <a href="#" class="lang-link" data-lang="ru">RU</a>
66        <a href="#" class="lang-link" data-lang="sr">SR</a>
67      </div>
68    </header>
69
70    <div class="card surface">
71      <div class="inline-row inline-row--sub">
72        <p class="helper">Multi-select supported. Each column will match the file name.</p>
73        <div class="status-block">
74          <div id="status"></div>
75          <div id="downloadLink"></div>
76        </div>
77      </div>
78
79      <div class="chart-tabs-row">
80        <div id="chartLegend" class="spectrum-tabs nav nav-tabs" role="tablist" aria-label="Open spectra"></div>
81        <div class="chart-actions">
82          <button id="searchLocalReferences" type="button" class="btn tonal chart-reference-search" hidden data-i18n="findReferenceMatches">Find reference matches</button>
83          <button id="openChartSettings" class="icon-btn chart-settings-trigger" type="button" title="Chart settings" aria-label="Chart settings" disabled>
84            <span class="material-symbols-outlined">tune</span>
85          </button>
86        </div>
87      </div>
88      <div class="chart-row is-hidden" id="chartRow">
89        <div class="chart-main">
90          <div id="zoneHint" class="zone-hint"></div>
91          <div class="chart-interaction-hint" data-i18n="chartInteractionHint">Ctrl/Cmd + scroll over the chart to zoom · middle mouse button to pan · arrow keys to pan</div>
92          <div id="chart"></div>
93        </div>
94      </div>
95    </div>
96
97    <aside class="reference-sidebar" id="referenceSearchCard" hidden aria-label="Reference search results">
98      <div class="reference-card-header">
99        <div>
100          <div class="eyebrow" data-i18n="referenceMatchesEyebrow">Whole-spectrum reference search</div>
101          <h3 data-i18n="referenceMatchesTitle">Reference matches</h3>
102          <p id="referenceSearchSpectrum" class="reference-spectrum-name"></p>
103        </div>
104        <span id="referenceSearchStatus" class="analysis-status"></span>
105      </div>
106      <div id="referenceSearchResult" class="reference-search-result" aria-live="polite"></div>
107    </aside>
108    <button id="referenceSidebarToggle" class="reference-sidebar-toggle" type="button" hidden aria-controls="referenceSearchCard" aria-expanded="false" title="Open reference results" aria-label="Open reference results">
109      <span class="material-symbols-outlined">chevron_left</span>
110    </button>
111
112    <dialog id="chartSettingsDialog" class="chart-settings-dialog">
113      <div class="dialog-header">
114        <div>
115          <div class="eyebrow">Chart settings</div>
116          <h3>View and processing</h3>
117        </div>
118        <button id="closeChartSettings" class="icon-btn" type="button" aria-label="Close chart settings">×</button>
119      </div>
120      <div id="chartControls" class="controls chart-settings-content">
121            <h3 data-i18n="chartSettings">Chart settings</h3>
122            <div class="control-group">
123              <label data-i18n="xRange">X range (reverse):</label>
124              <div class="inline-inputs">
125                <input id="xMax" type="number" value="4000" step="1" disabled>
126                <span>→</span>
127                <input id="xMin" type="number" value="500" step="1" disabled>
128              </div>
129            </div>
130            <div class="control-group">
131              <label data-i18n="yRange">Y range:</label>
132              <div class="inline-inputs">
133                <input id="yMin" type="number" step="0.1" placeholder="auto" disabled>
134                <span>→</span>
135                <input id="yMax" type="number" step="0.1" placeholder="auto" disabled>
136              </div>
137            </div>
138            <div class="control-group baseline-group">
139              <label data-i18n="baselineSection">Baseline fit</label>
140              <div class="baseline-controls">
141                <select id="baselineSeries"></select>
142                <input id="baselineDegree" type="number" min="1" max="6" step="1" value="2">
143                <button id="baselinePreview" type="button" class="btn tonal" disabled data-i18n="baselinePreview">Preview</button>
144                <button id="baselineApply" type="button" class="btn tonal" disabled data-i18n="baselineApply">Apply</button>
145                <button id="baselineRevert" type="button" class="btn tonal" disabled data-i18n="baselineRevert">Revert</button>
146              </div>
147            </div>
148            <div class="control-actions">
149              <button id="refreshChart" type="button" class="icon-btn" disabled title="Refresh chart" aria-label="Refresh chart">
150                <span class="material-symbols-outlined">refresh</span>
151              </button>
152              <button id="resetZoom" type="button" class="icon-btn" disabled title="Reset zoom" aria-label="Reset zoom">
153                <span class="material-symbols-outlined">zoom_out_map</span>
154              </button>
155            </div>
156      </div>
157    </dialog>
158
159    <div class="card surface peaks-card">
160      <div class="peaks-header">
161        <div>
162          <h3 data-i18n="peaksTitle">Peaks / stripes</h3>
163          <div id="peaksSpectrumTitle" class="selected-spectrum-title">No spectrum selected</div>
164        </div>
165        <div class="peaks-header-actions">
166          <button id="peakDetectionHelp" type="button" class="icon-btn detector-help" title="Peak detector guide" aria-label="Peak detector guide">
167            <span class="material-symbols-outlined">help</span>
168          </button>
169          <button id="togglePeaksTable" type="button" class="icon-btn" title="Collapse peaks table" aria-label="Collapse peaks table" aria-expanded="true" aria-controls="peaksTableWrap">
170            <span class="material-symbols-outlined">unfold_less</span>
171          </button>
172        </div>
173      </div>
174      <div class="peaks-control-layout">
175        <fieldset class="detector-fieldset detector-source-fieldset">
176          <legend data-i18n="peakSearchLabel">Peak search</legend>
177          <div class="detector-source-grid">
178            <div class="detector-source-heading">
179              <span data-i18n="detectorSignalLabel">Signal</span>
180              <div class="detector-icon-actions">
181                <button id="openPeakSettings" type="button" class="icon-btn detector-help" title="Peak processing settings" aria-label="Peak processing settings" disabled>
182                  <span class="material-symbols-outlined">tune</span>
183                </button>
184              </div>
185            </div>
186            <label class="detector-signal-field" for="detectorSignalType">
187              <select id="detectorSignalType" aria-label="Signal type">
188                <option value="unknown">Auto / unknown</option>
189                <option value="transmittance">%T transmittance</option>
190                <option value="absorbance">Absorbance / intensity</option>
191              </select>
192            </label>
193            <div class="detector-baseline-state">
194              <span data-i18n="detectorBaseline">Baseline</span>
195              <strong id="detectorBaselineSummary" class="detector-baseline-summary">raw mode</strong>
196            </div>
197          </div>
198        </fieldset>
199        <fieldset class="detector-fieldset detector-parameters-fieldset">
200          <legend data-i18n="detectorParameters">Detection parameters</legend>
201          <div class="detector-parameters-grid">
202            <label class="detector-field" for="detectorMinProminence"><span data-i18n="detectorProminence">Prominence</span><input id="detectorMinProminence" type="number" min="0" step="0.01" value="0.02" aria-label="Minimum prominence" title="Minimum prominence"></label>
203            <label class="detector-field" for="detectorMinSeparation"><span data-i18n="detectorDistance">Distance</span><input id="detectorMinSeparation" type="number" min="0.1" step="0.1" value="8" aria-label="Minimum separation cm-1" title="Minimum separation (cm-1)"></label>
204            <label class="detector-field" for="detectorSmoothingWindow"><span data-i18n="detectorSmooth">Smooth</span><input id="detectorSmoothingWindow" type="number" min="1" max="101" step="2" value="5" aria-label="Smoothing window" title="Smoothing window"></label>
205            <button id="detectPeaks" type="button" class="btn tonal detector-run-btn" disabled>Detect</button>
206          </div>
207          <small id="detectorStatus">Not run.</small>
208          <div id="detectorConnectionStatus" class="detector-connection-status" role="alert" hidden></div>
209        </fieldset>
210        <fieldset class="detector-fieldset peaks-actions-fieldset">
211          <legend data-i18n="peakActions">Peak table actions</legend>
212          <div class="peaks-actions">
213            <button id="addStripe" type="button" class="btn tonal" data-i18n="addStripe">Add stripe</button>
214            <button id="copyStripes" type="button" class="btn tonal" data-i18n="copyStripes">Copy table</button>
215            <button id="copyConfirmedPayload" type="button" class="btn tonal">Copy confirmed JSON</button>
216            <button id="analyzeConfirmed" type="button" class="btn primary">Analyze confirmed</button>
217          </div>
218          <label class="analysis-prompt-field" for="analysisPrompt">
219            <span data-i18n="analysisPromptLabel">Additional analysis context (optional)</span>
220            <textarea id="analysisPrompt" rows="2" maxlength="2000" data-i18n-placeholder="analysisPromptPlaceholder" placeholder="Add a question or context for the interpretation"></textarea>
221          </label>
222        </fieldset>
223      </div>
224      <p class="peaks-hint" data-i18n="peakHint">Click the chart to place a marker, then add it here.</p>
225      <div id="peaksTableWrap" class="peaks-table">
226        <div class="stripe-sets">
227          <button class="stripe-set-btn active" data-set="candidates">Candidates</button>
228          <button class="stripe-set-btn" data-set="confirmed">Confirmed</button>
229          <button id="confirmAllPeaks" type="button" class="stripe-table-action" disabled title="Confirm all candidates for the selected spectrum">Confirm all</button>
230        </div>
231        <div class="peaks-row peaks-head">
232          <span> </span>
233          <span data-i18n="colWave">Wavenumber</span>
234          <span data-i18n="colLabel">Label</span>
235          <span data-i18n="colTip">Tip</span>
236          <span> </span>
237        </div>
238        <div id="peaksEmpty" class="peaks-empty" data-i18n="peaksEmpty">No stripes yet. Set a marker and add it here.</div>
239        <div id="peaksBody" class="peaks-body"></div>
240      </div>
241    </div>
242    <dialog id="spectrumSettingsDialog" class="spectrum-settings-dialog">
243      <form method="dialog" id="spectrumSettingsForm">
244        <div class="dialog-header">
245          <div>
246            <div class="eyebrow">Spectrum settings</div>
247            <h3 id="spectrumSettingsTitle">Spectrum</h3>
248          </div>
249          <button type="button" class="icon-btn" id="closeSpectrumSettings" aria-label="Close settings">×</button>
250        </div>
251        <input id="spectrumSettingsId" type="hidden">
252        <label for="spectrumSettingsName">Display name</label>
253        <input id="spectrumSettingsName" type="text" maxlength="255">
254        <label for="spectrumSettingsOffset">Y offset</label>
255        <input id="spectrumSettingsOffset" type="number" step="0.1">
256        <label for="spectrumSettingsRole" data-i18n="spectrumRole">Role in series</label>
257        <select id="spectrumSettingsRole" aria-label="Role in series">
258          <option value="unknown" data-i18n="roleUnknown">Unknown</option>
259          <option value="before" data-i18n="roleBefore">Before</option>
260          <option value="after" data-i18n="roleAfter">After</option>
261          <option value="control" data-i18n="roleControl">Control</option>
262          <option value="repeat" data-i18n="roleRepeat">Repeat</option>
263        </select>
264        <div class="dialog-actions">
265          <button type="button" class="btn danger" id="removeSpectrumFromDialog">Remove spectrum</button>
266          <button type="submit" class="btn primary" id="saveSpectrumSettings">Save</button>
267        </div>
268      </form>
269    </dialog>
270    <dialog id="peakDetectionHelpDialog" class="peak-help-dialog">
271      <div class="dialog-header">
272        <div>
273          <div class="eyebrow" data-i18n="peakHelpEyebrow">Peak search</div>
274          <h3 data-i18n="peakHelpTitle">Peak detector guide</h3>
275        </div>
276        <button id="closePeakDetectionHelp" class="icon-btn" type="button" aria-label="Close peak detector guide">×</button>
277      </div>
278      <p class="peak-help-intro" data-i18n="peakHelpIntro">These settings affect how automatic peaks are selected for the active spectrum.</p>
279      <div class="peak-help-list">
280        <section><h4 data-i18n="peakHelpSignalType">Signal type</h4><p data-i18n="peakHelpSignalTypeText">Transmittance (%T) is converted to absorbance, so absorption bands are detected as peaks. For absorbance or intensity, the values are used directly.</p></section>
281        <section><h4 data-i18n="peakHelpBaseline">Baseline method</h4><p data-i18n="peakHelpBaselineText">Removes slow background curvature before peak search. arPLS is the default. For very broad bands, compare the corrected curve and try Linear or None if the band is being removed.</p></section>
282        <section><h4 data-i18n="peakHelpProminence">Prominence</h4><p data-i18n="peakHelpProminenceText">Minimum contrast against the local baseline. Increase it to remove noise and weak shoulders; decrease it to find weaker bands.</p></section>
283        <section><h4 data-i18n="peakHelpDistance">Distance</h4><p data-i18n="peakHelpDistanceText">Minimum separation between peaks in cm⁻¹. Increase it when one broad band is split into many nearby peaks.</p></section>
284        <section><h4 data-i18n="peakHelpSmooth">Smooth</h4><p data-i18n="peakHelpSmoothText">Moving-average window in data points. Increase it for noisy spectra, but a large value can flatten narrow peaks. Use an odd number.</p></section>
285      </div>
286      <p class="peak-help-tip" data-i18n="peakHelpQuick">Quick start: try Prominence 0.02, Distance 8, Smooth 5, then adjust one setting at a time.</p>
287    </dialog>
288    <dialog id="peakDetectorSettingsDialog" class="peak-detector-settings-dialog">
289      <div class="dialog-header">
290        <div>
291          <div class="eyebrow" data-i18n="detectorBaselineEyebrow">Peak processing</div>
292          <h3 data-i18n="detectorBaselineTitle">Baseline correction</h3>
293        </div>
294        <button id="closePeakSettings" class="icon-btn" type="button" aria-label="Close baseline settings">×</button>
295      </div>
296      <p class="peak-help-intro" data-i18n="detectorBaselineIntro">Choose how the slow background is removed from the active spectrum. Apply the correction before searching peaks.</p>
297      <label for="detectorBaselineMethod" data-i18n="detectorBaseline">Baseline method</label>
298      <select id="detectorBaselineMethod" aria-label="Baseline method">
299        <option value="arpls">arPLS</option>
300        <option value="airpls">airPLS</option>
301        <option value="asls">AsLS</option>
302        <option value="snip">SNIP</option>
303        <option value="rubberband">Rubberband</option>
304        <option value="linear">Linear</option>
305        <option value="none">None</option>
306      </select>
307      <div class="peak-detector-settings-note" data-i18n="detectorBaselineNote">The main chart will switch to the corrected curve in the original display scale. Peak search will use the same applied correction.</div>
308      <div id="peakProcessingMeta" class="peak-processing-meta"></div>
309      <div id="peakProcessingChart" class="peak-processing-chart"></div>
310      <p class="peak-processing-hint" data-i18n="peakProcessingHint">The red line is the baseline used by the fine detector. The green curve is searched for peaks. The broad-pass detector keeps broad bands separately.</p>
311      <div class="dialog-actions">
312        <button id="cancelPeakSettings" type="button" class="btn tonal" data-i18n="cancel">Cancel</button>
313        <button id="applyPeakBaseline" type="button" class="btn primary" data-i18n="applyBaseline">Apply baseline</button>
314      </div>
315    </dialog>
316    <div class="card surface analysis-card" id="analysisCard" hidden>
317      <div class="peaks-header">
318        <h3>LLM interpretation</h3>
319        <span id="analysisStatus" class="analysis-status"></span>
320      </div>
321      <div id="analysisResult" class="analysis-result" aria-live="polite"></div>
322    </div>
323    <dialog id="appSettingsDialog" class="app-settings-dialog">
324      <form id="appSettingsForm" method="dialog">
325        <div class="dialog-header">
326          <div>
327            <div class="eyebrow" data-i18n="settingsEyebrow">Local profile</div>
328            <h3 data-i18n="settingsTitle">Application settings</h3>
329          </div>
330          <button id="closeAppSettings" class="icon-btn" type="button" aria-label="Close settings">×</button>
331        </div>
332        <p class="settings-note" data-i18n="settingsSecretNote">Settings are stored only in this browser. Do not import an untrusted settings file.</p>
333        <fieldset class="settings-fieldset">
334          <legend data-i18n="settingsModeTitle">Mode</legend>
335          <label for="settingsMode"><span data-i18n="settingsMode">Runtime mode</span>
336            <select id="settingsMode"><option value="development" data-i18n="settingsDevelopment">Development</option><option value="production" data-i18n="settingsProduction">Production</option></select>
337          </label>
338        </fieldset>
339        <fieldset class="settings-fieldset">
340          <legend data-i18n="settingsConnectionsTitle">Service connections</legend>
341          <div class="settings-grid">
342            <label for="settingsAnalysisApi"><span data-i18n="settingsAnalysisApi">Analysis API URL</span><input id="settingsAnalysisApi" type="url" inputmode="url" placeholder="https://…/api/analyze"></label>
343            <label for="settingsPeakApi"><span data-i18n="settingsPeakApi">Peak detector API URL</span><input id="settingsPeakApi" type="url" inputmode="url" placeholder="https://…/api/peaks/detect"></label>
344            <label for="settingsReferenceApi"><span data-i18n="settingsReferenceApi">Reference search API URL</span><input id="settingsReferenceApi" type="url" inputmode="url" placeholder="http://127.0.0.1:8088/api/v1/search"></label>
345            <label for="settingsApiCredentials"><span data-i18n="settingsCredentials">API credentials</span><select id="settingsApiCredentials"><option value="omit" data-i18n="settingsCredentialsOmit">Do not send cookies</option><option value="include" data-i18n="settingsCredentialsInclude">Send cookies</option></select></label>
346          </div>
347        </fieldset>
348        <fieldset class="settings-fieldset">
349          <legend data-i18n="settingsLlmTitle">LLM preferences</legend>
350          <p class="settings-note" data-i18n="settingsByokNote">A personal AI key is sent only to a server with BYOK explicitly enabled. It is never included in logs.</p>
351          <div class="settings-grid settings-grid--three">
352            <label for="settingsLlmProvider"><span data-i18n="settingsProvider">Provider</span><select id="settingsLlmProvider"><option value="server" data-i18n="settingsProviderServer">Server default</option><option value="gemini">Gemini</option><option value="mistral">Mistral</option><option value="mock">Mock</option></select></label>
353            <label for="settingsLlmModel"><span data-i18n="settingsModel">Model</span><input id="settingsLlmModel" type="text" maxlength="256"></label>
354            <label for="settingsLlmApiKey"><span data-i18n="settingsLlmApiKey">Personal AI API key</span><input id="settingsLlmApiKey" type="password" autocomplete="off" maxlength="2048"></label>
355          </div>
356        </fieldset>
357        <fieldset class="settings-fieldset">
358          <legend data-i18n="settingsTokensTitle">Access tokens</legend>
359          <p class="settings-note settings-token-note"><span data-i18n="settingsTokenHelp">Reference token is SERVICE_TOKEN from the reference-service .env. The analysis token is needed only if your own analysis API requires Bearer authentication.</span> <a href="https://github.com/ksider/ftir-reference-service" target="_blank" rel="noopener" data-i18n="settingsReferenceRepoLink">Reference service repository</a> · <a href="https://github.com/ksider/FTIR-file-Merger/tree/main/server" target="_blank" rel="noopener" data-i18n="settingsAnalysisRepoLink">
359Analysis server repository</a></p>
360          <div class="settings-grid">
361            <label for="settingsAnalysisToken"><span data-i18n="settingsAnalysisToken">Optional Bearer token for analysis API</span><input id="settingsAnalysisToken" type="password" autocomplete="off" maxlength="2048"></label>
362            <label for="settingsReferenceToken"><span data-i18n="settingsReferenceToken">SERVICE_TOKEN from reference service .env</span><input id="settingsReferenceToken" type="password" autocomplete="off" maxlength="2048"></label>
363          </div>
364        </fieldset>
365        <div class="settings-actions">
366          <button id="exportUserSettings" type="button" class="btn tonal" data-i18n="settingsExport">Export without keys</button>
367          <button id="exportUserSettingsWithSecrets" type="button" class="btn tonal" data-i18n="settingsExportSecrets">Export encrypted keys</button>
368          <button id="importUserSettings" type="button" class="btn tonal" data-i18n="settingsImport">Import settings</button>
369          <button id="resetUserSettings" type="button" class="btn danger" data-i18n="settingsReset">Reset profile</button>
370          <button type="submit" class="btn primary" data-i18n="settingsSave">Save and reload</button>
371        </div>
372      </form>
373    </dialog>
374    <footer class="footer surface">
375      <div class="footer-links">
376        <a id="footerSite" href="#" rel="noopener" target="_blank">nikolaysemenov.ru</a>
377        <span class="dot">•</span>
378        <a id="footerGithub" href="#" rel="noopener" target="_blank">GitHub</a>
379        <span class="dot">•</span>
380        <a id="footerCoffee" href="#" rel="noopener" target="_blank">Сoindrop</a>
381      </div>
382    </footer>
383  </div>
384
385  
385<script src="./config.js"></script>
385
386  
386<script src="./settings.js"></script>
386
387  
387<script src="./peak-db.js"></script>
387
388  
388<script src="./app.js"></script>
388
389
390  <!-- Google tag (gtag.js) -->
vendor: 64 bytes, lines 390-391
390
391<script async src="https://www.googletagmanager.com/gtag/js?id=
391G-X25WXQ0818
vendor: 12 bytes, line 391
391"></script>
392<script>
393  
vendor: 134 bytes, lines 393-397
393window.dataLayer = window.dataLayer || [];
394  function gtag(){dataLayer.push(arguments);}
395  gtag('js', new Date());
396
397  gtag('config', '
397G-X25WXQ0818
vendor: 4 bytes, line 397
397');
398</script>
vendor: 1 bytes, line 398
398
399<script type="module" src="https://static.cloudflareinsights.com/beacon.min.js/v4bc70e2c01a94c73b74392e4234840661791215815920" integrity="sha512-L0ha0OXavK/8okipN9F8BtP84dg9DUhPERbBXzwI6dgTA55d2+yweo3pn5CSFYs45/r8md2+xvUPtTdvTNRfjA==" data-cf-beacon='{"version":"2024.11.0","token":"f2ad2398c11046f08e3d89180cb7f86c","r":1,"spa":2}' crossorigin="anonymous"></script>
399
400</body>
401</html>

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.