web/src/lib/components/ProfileSummary.svelte 4.5 K raw
1
<script>
2
  import { aggregateDigraphs } from '../aggregation';
3
4
  export let digraphs = [];
5
  export let metadata = null;
6
7
  let showAll = false;
8
9
  $: aggregations = aggregateDigraphs(digraphs);
10
  $: displayAggs = showAll ? aggregations : aggregations.slice(0, 15);
11
  $: hasMore = aggregations.length > 15;
12
13
  function fmt(stats) {
14
    return `${stats.mean} ±${stats.std}`;
15
  }
16
</script>
17
18
{#if digraphs.length === 0}
19
  <div class="empty">
20
    <p>no digraphs captured yet</p>
21
    <span class="empty-sub">type in the textarea on the capture tab to build your profile</span>
22
  </div>
23
{:else}
24
  {#if metadata}
25
    <div class="metadata-bar">
26
      <div class="meta-item">
27
        <span class="meta-val">{metadata.totalKeystrokes}</span>
28
        <span class="meta-label">keystrokes</span>
29
      </div>
30
      <div class="meta-item">
31
        <span class="meta-val">{metadata.backspaceCount}</span>
32
        <span class="meta-label">backspaces</span>
33
      </div>
34
      <div class="meta-item">
35
        <span class="meta-val">{metadata.pauseCount}</span>
36
        <span class="meta-label">pauses</span>
37
      </div>
38
      <div class="meta-item">
39
        <span class="meta-val">{metadata.avgTypingSpeed}<span class="unit-sm"> cpm</span></span>
40
        <span class="meta-label">speed</span>
41
      </div>
42
      <div class="meta-item">
43
        <span class="meta-val">{(metadata.sessionDurationMs / 1000).toFixed(1)}<span class="unit-sm">s</span></span>
44
        <span class="meta-label">duration</span>
45
      </div>
46
      <div class="meta-item">
47
        <span class="meta-val">{aggregations.length}</span>
48
        <span class="meta-label">unique pairs</span>
49
      </div>
50
    </div>
51
  {/if}
52
53
  <div class="agg-table-wrap">
54
    <table class="agg-table">
55
      <thead>
56
        <tr>
57
          <th>keys</th>
58
          <th>n</th>
59
          <th>HT1</th>
60
          <th>HT2</th>
61
          <th>PP</th>
62
          <th>RR</th>
63
          <th>PR</th>
64
          <th>RP</th>
65
        </tr>
66
      </thead>
67
      <tbody>
68
        {#each displayAggs as a}
69
          <tr>
70
            <td class="keys-cell">{a.normalizedKeys}</td>
71
            <td class="count-cell">{a.count}</td>
72
            <td>{fmt(a.holdTime1)}</td>
73
            <td>{fmt(a.holdTime2)}</td>
74
            <td>{fmt(a.pressPress)}</td>
75
            <td>{fmt(a.releaseRelease)}</td>
76
            <td>{fmt(a.pressRelease)}</td>
77
            <td>{fmt(a.releasePress)}</td>
78
          </tr>
79
        {/each}
80
      </tbody>
81
    </table>
82
  </div>
83
84
  {#if hasMore}
85
    <button class="toggle-btn" on:click={() => (showAll = !showAll)}>
86
      {showAll ? `show top 15` : `show all ${aggregations.length}`}
87
    </button>
88
  {/if}
89
{/if}
90
91
<style>
92
  .empty {
93
    display: flex;
94
    flex-direction: column;
95
    align-items: center;
96
    justify-content: center;
97
    padding: 4rem 2rem;
98
    border: 1px dashed #333;
99
    gap: 0.5rem;
100
  }
101
102
  .empty p {
103
    font-size: 14px;
104
    color: #888;
105
  }
106
107
  .empty-sub {
108
    font-size: 11px;
109
    color: #555;
110
  }
111
112
  .metadata-bar {
113
    display: grid;
114
    grid-template-columns: repeat(6, 1fr);
115
    border: 1px solid #333;
116
  }
117
118
  .meta-item {
119
    display: flex;
120
    flex-direction: column;
121
    align-items: center;
122
    padding: 0.75rem 0.5rem;
123
    border-right: 1px solid #333;
124
  }
125
126
  .meta-item:last-child {
127
    border-right: none;
128
  }
129
130
  .meta-val {
131
    font-size: 14px;
132
    font-weight: 700;
133
  }
134
135
  .meta-label {
136
    font-size: 10px;
137
    color: #888;
138
    margin-top: 0.15rem;
139
  }
140
141
  .unit-sm {
142
    font-size: 10px;
143
    color: #888;
144
    font-weight: 400;
145
  }
146
147
  .agg-table-wrap {
148
    border: 1px solid #333;
149
    max-height: 400px;
150
    overflow-y: auto;
151
    overflow-x: auto;
152
  }
153
154
  .agg-table {
155
    width: 100%;
156
    border-collapse: collapse;
157
    font-size: 12px;
158
  }
159
160
  .agg-table th,
161
  .agg-table td {
162
    padding: 0.4rem 0.5rem;
163
    text-align: center;
164
    border-bottom: 1px solid #333;
165
    white-space: nowrap;
166
  }
167
168
  .agg-table th {
169
    position: sticky;
170
    top: 0;
171
    background: #121113;
172
    color: #888;
173
    font-weight: 400;
174
    font-size: 10px;
175
    text-transform: uppercase;
176
    letter-spacing: 0.05em;
177
  }
178
179
  .agg-table .keys-cell {
180
    text-align: left;
181
    font-weight: 700;
182
  }
183
184
  .agg-table .count-cell {
185
    color: #888;
186
  }
187
188
  .toggle-btn {
189
    align-self: flex-end;
190
    font-size: 12px;
191
    padding: 4px 8px;
192
  }
193
194
  @media (max-width: 480px) {
195
    .metadata-bar {
196
      grid-template-columns: repeat(3, 1fr);
197
    }
198
199
    .meta-item:nth-child(3) {
200
      border-right: none;
201
    }
202
203
    .meta-item:nth-child(1),
204
    .meta-item:nth-child(2),
205
    .meta-item:nth-child(3) {
206
      border-bottom: 1px solid #333;
207
    }
208
  }
209
</style>