| <script lang="ts"> |
| |
| |
| |
| |
| import type { WizardState } from '../../lib/stores/app-state'; |
| |
| let { wizard }: { wizard: WizardState } = $props(); |
| |
| const tokens = $derived(wizard.liveTokens); |
| const running = $derived(wizard.status === 'running'); |
| const greens = $derived(tokens.filter((t) => t.green === true).length); |
| const key2 = $derived(tokens.filter((t) => t.keyId === 2).length); |
| |
| let box: HTMLDivElement | undefined = $state(); |
| let pinned = $state(true); |
| |
| |
| $effect(() => { |
| void tokens.length; |
| if (box && pinned) box.scrollTop = box.scrollHeight; |
| }); |
| |
| function onScroll() { |
| if (!box) return; |
| pinned = box.scrollHeight - box.scrollTop - box.clientHeight < 24; |
| } |
| |
| function tokenClass(green?: boolean, keyId?: 1 | 2): string { |
| if (green === true) return 'tok-green'; |
| if (green === false) return 'tok-red'; |
| if (keyId === 2) return 'key2'; |
| return ''; |
| } |
| </script> |
|
|
| {#if running || tokens.length > 0} |
| <div class="live"> |
| <div class="head small dim"> |
| <span class:pulse={running}>{running ? 'sampling' : 'sampled'}</span> |
| <span class="mono">{tokens.length} tokens</span> |
| {#if wizard.algorithm === 'kirchenbauer' && tokens.length > 0} |
| <span class="mono"> |
| {greens} green ({((greens / tokens.length) * 100).toFixed(0)}%) |
| </span> |
| {/if} |
| {#if wizard.algorithm === 'textseal' && tokens.length > 0} |
| <span class="mono">key₂ ×{key2}</span> |
| {/if} |
| {#if wizard.retries > 0} |
| <span class="mono">{wizard.retries} sentence retries</span> |
| {/if} |
| {#if !pinned} |
| <button class="follow" onclick={() => { pinned = true; if (box) box.scrollTop = box.scrollHeight; }}> |
| follow |
| </button> |
| {/if} |
| </div> |
| <div class="text-box plain" bind:this={box} onscroll={onScroll}> |
| {#each tokens as t (t.index)}<span |
| class={tokenClass(t.green, t.keyId)} |
| title={`step ${t.index}${t.entropy !== undefined ? ` · H=${t.entropy.toFixed(2)}` : ''}${t.keyId ? ` · key${t.keyId}` : ''}`} |
| >{t.text}</span |
| >{/each}{#if running}<span class="caret">|</span>{/if} |
| </div> |
| </div> |
| {/if} |
|
|
| <style> |
| .live { |
| display: flex; |
| flex-direction: column; |
| gap: 6px; |
| } |
| .head { |
| display: flex; |
| align-items: center; |
| gap: 12px; |
| flex-wrap: wrap; |
| } |
| .follow { |
| padding: 1px 8px; |
| font-size: 11.5px; |
| } |
| .text-box { |
| white-space: pre-wrap; |
| font-size: 14px; |
| } |
| .key2 { |
| background: var(--amber-bg); |
| box-shadow: inset 0 -2px 0 #e6cd94; |
| border-radius: 2px; |
| } |
| .caret { |
| color: var(--accent); |
| animation: blink 1s steps(2, start) infinite; |
| } |
| @keyframes blink { |
| to { |
| visibility: hidden; |
| } |
| } |
| .pulse::after { |
| content: '…'; |
| animation: blink 1.2s steps(3, start) infinite; |
| } |
| </style> |
|
|