web/src/lib/components/HumannessView.svelte 4.7 K raw
1
<script>
2
  import { analyzeHumanness } from '../humanness';
3
4
  export let digraphs = [];
5
  export let metadata = {};
6
7
  $: result = analyzeHumanness(digraphs, metadata);
8
9
  const subScoreLabels = {
10
    timingVariance: {
11
      name: 'timing variance',
12
      desc: 'variation in key-to-key timing per digraph pair',
13
    },
14
    correctionRate: {
15
      name: 'correction rate',
16
      desc: 'backspace usage relative to total keystrokes',
17
    },
18
    pauseDistribution: {
19
      name: 'pause distribution',
20
      desc: 'presence and variety of natural thinking pauses',
21
    },
22
    distributionShape: {
23
      name: 'distribution shape',
24
      desc: 'right-skewed timing distribution (log-normal)',
25
    },
26
    flightTimeNegativity: {
27
      name: 'flight time negativity',
28
      desc: 'key overlap from pressing next key before releasing previous',
29
    },
30
    burstPatterns: {
31
      name: 'burst patterns',
32
      desc: 'variance in typing burst lengths between pauses',
33
    },
34
  };
35
36
  const subScoreKeys = Object.keys(subScoreLabels);
37
38
  function verdictColor(verdict) {
39
    if (verdict === 'likely human') return '#4ade80';
40
    if (verdict === 'uncertain') return '#facc15';
41
    return '#ff6b6b';
42
  }
43
44
  function barColor(score) {
45
    if (score >= 70) return '#4ade80';
46
    if (score >= 40) return '#facc15';
47
    return '#ff6b6b';
48
  }
49
</script>
50
51
{#if digraphs.length < 20}
52
  <div class="empty">
53
    <p>keep typing...</p>
54
    <span class="empty-sub">{20 - digraphs.length} more digraphs needed for analysis</span>
55
  </div>
56
{:else if result}
57
  <div class="humanness">
58
    <div class="score-card">
59
      <div class="main-score">
60
        <span class="score-value" style="color: {verdictColor(result.verdict)}">{result.score}</span>
61
        <span class="score-label">human-ness</span>
62
      </div>
63
      <div class="verdict-section">
64
        <span class="verdict" style="color: {verdictColor(result.verdict)}">{result.verdict}</span>
65
        {#if metadata.pastedCharCount > 0}
66
          <span class="paste-note">
67
            {metadata.pasteCount} paste{metadata.pasteCount !== 1 ? 's' : ''} detected ({metadata.pastedCharCount} chars)
68
          </span>
69
        {/if}
70
      </div>
71
    </div>
72
73
    <div class="bars">
74
      {#each subScoreKeys as key}
75
        <div class="bar-row">
76
          <div class="bar-header">
77
            <span class="bar-name">{subScoreLabels[key].name}</span>
78
            <span class="bar-value" style="color: {barColor(result.subScores[key])}">{result.subScores[key]}</span>
79
          </div>
80
          <div class="bar-track">
81
            <div
82
              class="bar-fill"
83
              style="width: {result.subScores[key]}%; background: {barColor(result.subScores[key])}"
84
            ></div>
85
          </div>
86
          <span class="bar-desc">{subScoreLabels[key].desc}</span>
87
        </div>
88
      {/each}
89
    </div>
90
  </div>
91
{/if}
92
93
<style>
94
  .empty {
95
    display: flex;
96
    flex-direction: column;
97
    align-items: center;
98
    justify-content: center;
99
    padding: 4rem 2rem;
100
    border: 1px dashed #333;
101
    gap: 0.5rem;
102
  }
103
104
  .empty p {
105
    font-size: 14px;
106
    color: #888;
107
  }
108
109
  .empty-sub {
110
    font-size: 11px;
111
    color: #555;
112
  }
113
114
  .humanness {
115
    display: flex;
116
    flex-direction: column;
117
    gap: 1.5rem;
118
  }
119
120
  .score-card {
121
    display: flex;
122
    border: 1px solid #333;
123
  }
124
125
  .main-score {
126
    display: flex;
127
    flex-direction: column;
128
    align-items: center;
129
    justify-content: center;
130
    padding: 1.5rem 2rem;
131
    border-right: 1px solid #333;
132
    min-width: 140px;
133
  }
134
135
  .score-value {
136
    font-size: 48px;
137
    font-weight: 700;
138
    line-height: 1;
139
  }
140
141
  .score-label {
142
    font-size: 10px;
143
    color: #888;
144
    margin-top: 0.35rem;
145
    text-transform: uppercase;
146
    letter-spacing: 0.05em;
147
  }
148
149
  .verdict-section {
150
    display: flex;
151
    flex-direction: column;
152
    justify-content: center;
153
    padding: 1rem 1.5rem;
154
    gap: 0.5rem;
155
  }
156
157
  .verdict {
158
    font-size: 18px;
159
    font-weight: 700;
160
    text-transform: uppercase;
161
    letter-spacing: 0.05em;
162
  }
163
164
  .paste-note {
165
    font-size: 10px;
166
    color: #555;
167
  }
168
169
  .bars {
170
    display: flex;
171
    flex-direction: column;
172
    gap: 1rem;
173
  }
174
175
  .bar-row {
176
    display: flex;
177
    flex-direction: column;
178
    gap: 0.25rem;
179
  }
180
181
  .bar-header {
182
    display: flex;
183
    justify-content: space-between;
184
    align-items: baseline;
185
  }
186
187
  .bar-name {
188
    font-size: 12px;
189
    font-weight: 700;
190
  }
191
192
  .bar-value {
193
    font-size: 13px;
194
    font-weight: 700;
195
  }
196
197
  .bar-track {
198
    width: 100%;
199
    height: 6px;
200
    background: #333;
201
  }
202
203
  .bar-fill {
204
    height: 100%;
205
    transition: width 0.3s ease;
206
  }
207
208
  .bar-desc {
209
    font-size: 10px;
210
    color: #555;
211
  }
212
213
  @media (max-width: 480px) {
214
    .score-card {
215
      flex-direction: column;
216
    }
217
218
    .main-score {
219
      border-right: none;
220
      border-bottom: 1px solid #333;
221
      padding: 1rem;
222
    }
223
  }
224
</style>