web/src/App.svelte 4.7 K raw
1
<script>
2
  import { round } from './lib/utils';
3
  import TabBar from './lib/components/TabBar.svelte';
4
  import CapturePanel from './lib/components/CapturePanel.svelte';
5
  import ProfileSummary from './lib/components/ProfileSummary.svelte';
6
  import ProfileManager from './lib/components/ProfileManager.svelte';
7
  import ComparisonView from './lib/components/ComparisonView.svelte';
8
  import HumannessView from './lib/components/HumannessView.svelte';
9
10
  let pendingKeys = new Map();
11
  let lastCompleted = null;
12
  let digraphs = [];
13
  let typedText = '';
14
  let activeTab = 'capture';
15
16
  // Session metadata
17
  let totalKeystrokes = 0;
18
  let backspaceCount = 0;
19
  let pauseCount = 0;
20
  let firstKeydownTime = null;
21
  let lastKeyupTime = null;
22
  let lastKeydownTime = null;
23
  let pasteCount = 0;
24
  let pastedCharCount = 0;
25
26
  $: sessionDurationMs = (firstKeydownTime && lastKeyupTime)
27
    ? Math.round(lastKeyupTime - firstKeydownTime)
28
    : 0;
29
30
  $: avgTypingSpeed = (sessionDurationMs > 0)
31
    ? Math.round(totalKeystrokes / (sessionDurationMs / 60000))
32
    : 0;
33
34
  $: sessionMetadata = {
35
    totalKeystrokes,
36
    backspaceCount,
37
    pauseCount,
38
    avgTypingSpeed,
39
    sessionDurationMs,
40
    pasteCount,
41
    pastedCharCount,
42
  };
43
44
  const MODIFIER_KEYS = new Set(['Shift', 'Control', 'Alt', 'Meta', 'CapsLock']);
45
46
  function handleKeyDown(e) {
47
    if (MODIFIER_KEYS.has(e.key)) return;
48
    if (pendingKeys.has(e.key)) return;
49
50
    const now = performance.now();
51
    pendingKeys.set(e.key, { pressTime: now });
52
53
    // Session metadata tracking
54
    totalKeystrokes++;
55
    if (e.key === 'Backspace') backspaceCount++;
56
    if (!firstKeydownTime) firstKeydownTime = now;
57
    if (lastKeydownTime && (now - lastKeydownTime) > 500) pauseCount++;
58
    lastKeydownTime = now;
59
  }
60
61
  function handleKeyUp(e) {
62
    if (MODIFIER_KEYS.has(e.key)) return;
63
    const pending = pendingKeys.get(e.key);
64
    if (!pending) return;
65
    pendingKeys.delete(e.key);
66
67
    const releaseTime = performance.now();
68
    lastKeyupTime = releaseTime;
69
    const currentEvent = { key: e.key, pressTime: pending.pressTime, releaseTime };
70
71
    if (lastCompleted) {
72
      const k1 = lastCompleted;
73
      const k2 = currentEvent;
74
      const holdTime1 = round(k1.releaseTime - k1.pressTime);
75
      const holdTime2 = round(k2.releaseTime - k2.pressTime);
76
      const pressPress = round(k2.pressTime - k1.pressTime);
77
      const releaseRelease = round(k2.releaseTime - k1.releaseTime);
78
      const pressRelease = round(k2.releaseTime - k1.pressTime);
79
      const releasePress = round(k2.pressTime - k1.releaseTime);
80
81
      const label = (k) => (k === ' ' ? '\u2423' : k);
82
      digraphs = [
83
        {
84
          id: Date.now(),
85
          keys: `${label(k1.key)} \u2192 ${label(k2.key)}`,
86
          key1: k1.key,
87
          key2: k2.key,
88
          holdTime1,
89
          holdTime2,
90
          pressPress,
91
          releaseRelease,
92
          pressRelease,
93
          releasePress,
94
        },
95
        ...digraphs,
96
      ];
97
    }
98
99
    lastCompleted = currentEvent;
100
  }
101
102
  function handlePaste(e) {
103
    const nativeEvent = e.detail;
104
    pasteCount++;
105
    const text = nativeEvent.clipboardData?.getData('text') || '';
106
    pastedCharCount += text.length;
107
  }
108
109
  function clear() {
110
    digraphs = [];
111
    lastCompleted = null;
112
    pendingKeys = new Map();
113
    typedText = '';
114
    totalKeystrokes = 0;
115
    backspaceCount = 0;
116
    pauseCount = 0;
117
    firstKeydownTime = null;
118
    lastKeyupTime = null;
119
    lastKeydownTime = null;
120
    pasteCount = 0;
121
    pastedCharCount = 0;
122
  }
123
</script>
124
125
<svelte:window on:keydown={handleKeyDown} on:keyup={handleKeyUp} />
126
127
<main>
128
  <div class="header">
129
    <h1>keystroke dynamics</h1>
130
    <p class="sub">type in the textarea to capture digraph timing features</p>
131
  </div>
132
133
  <TabBar bind:activeTab />
134
135
  {#if activeTab === 'capture'}
136
    <CapturePanel bind:digraphs bind:typedText on:paste={handlePaste} />
137
    {#if digraphs.length > 0}
138
      <button class="clear-btn" on:click={clear}>clear session</button>
139
    {/if}
140
  {:else if activeTab === 'profile'}
141
    <ProfileSummary {digraphs} metadata={sessionMetadata} />
142
    <ProfileManager {digraphs} metadata={sessionMetadata} />
143
  {:else if activeTab === 'compare'}
144
    <ComparisonView {digraphs} />
145
  {:else if activeTab === 'humanness'}
146
    <HumannessView {digraphs} metadata={sessionMetadata} />
147
  {/if}
148
</main>
149
150
<style>
151
  main {
152
    width: 100%;
153
    padding: 2rem 1rem 4rem;
154
    display: flex;
155
    flex-direction: column;
156
    gap: 1.5rem;
157
  }
158
159
  .header {
160
    display: flex;
161
    flex-direction: column;
162
    gap: 0.5rem;
163
  }
164
165
  h1 {
166
    font-size: 16px;
167
    font-weight: 700;
168
    letter-spacing: 0.05em;
169
    line-height: 1;
170
  }
171
172
  .sub {
173
    font-size: 12px;
174
    color: #888;
175
  }
176
177
  .clear-btn {
178
    align-self: flex-end;
179
    font-size: 12px;
180
    padding: 4px 8px;
181
  }
182
183
</style>