File size: 3,078 Bytes
c126239
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
<script lang="ts">
  /**
   * The decoding as it happens: each sampled token appears the moment the
   * worker chooses it, already carrying the mark the watermark gave it.
   */
  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);

  // Follow the tail while it writes, unless the reader has scrolled away.
  $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>