web/src/lib/components/ComparisonView.svelte 7.9 K raw
1
<script>
2
  import { aggregateDigraphs } from '../aggregation';
3
  import { compareSession } from '../comparison';
4
  import { loadProfiles } from '../storage';
5
6
  export let digraphs = [];
7
8
  let profiles = loadProfiles();
9
  let selectedProfileId = '';
10
  let result = null;
11
12
  $: sessionAggs = aggregateDigraphs(digraphs);
13
  $: selectedProfile = profiles.find((p) => p.id === selectedProfileId) || null;
14
  $: canCompare = selectedProfile && sessionAggs.length >= 5;
15
16
  function refreshProfiles() {
17
    profiles = loadProfiles();
18
    if (selectedProfileId && !profiles.find((p) => p.id === selectedProfileId)) {
19
      selectedProfileId = '';
20
      result = null;
21
    }
22
  }
23
24
  function handleCompare() {
25
    if (!selectedProfile) return;
26
    result = compareSession(sessionAggs, selectedProfile.aggregations);
27
  }
28
29
  function confidenceColor(confidence) {
30
    if (confidence === 'high') return '#4ade80';
31
    if (confidence === 'medium') return '#facc15';
32
    if (confidence === 'low') return '#fb923c';
33
    return '#888';
34
  }
35
36
  function matchColor(percent) {
37
    if (percent >= 70) return '#4ade80';
38
    if (percent >= 40) return '#facc15';
39
    return '#ff6b6b';
40
  }
41
42
  // Refresh profiles when the component becomes visible
43
  $: if (digraphs) refreshProfiles();
44
</script>
45
46
{#if digraphs.length === 0}
47
  <div class="empty">
48
    <p>no digraphs captured</p>
49
    <span class="empty-sub">type in the textarea on the capture tab first</span>
50
  </div>
51
{:else}
52
  <div class="comparison">
53
    <div class="select-row">
54
      <select bind:value={selectedProfileId} class="profile-select">
55
        <option value="">select a profile to compare against</option>
56
        {#each profiles as profile (profile.id)}
57
          <option value={profile.id}>{profile.name}</option>
58
        {/each}
59
      </select>
60
      <button
61
        on:click={handleCompare}
62
        disabled={!canCompare}
63
        class="compare-btn"
64
      >
65
        compare
66
      </button>
67
    </div>
68
69
    {#if selectedProfile && sessionAggs.length < 5}
70
      <span class="hint">need at least 5 unique digraph pairs to compare</span>
71
    {/if}
72
73
    {#if profiles.length === 0}
74
      <span class="hint">save a profile first on the profile tab</span>
75
    {/if}
76
77
    {#if result}
78
      <div class="results">
79
        <div class="score-card">
80
          <div class="similarity">
81
            <span class="similarity-value" style="color: {matchColor(result.similarityPercent)}">{result.similarityPercent}%</span>
82
            <span class="similarity-label">similarity</span>
83
          </div>
84
          <div class="score-details">
85
            <div class="detail-row">
86
              <span class="detail-label">distance</span>
87
              <span class="detail-value">{result.overallDistance}</span>
88
            </div>
89
            <div class="detail-row">
90
              <span class="detail-label">shared pairs</span>
91
              <span class="detail-value">{result.sharedCount}</span>
92
            </div>
93
            <div class="detail-row">
94
              <span class="detail-label">confidence</span>
95
              <span class="detail-value confidence-badge" style="color: {confidenceColor(result.confidence)}">{result.confidence}</span>
96
            </div>
97
          </div>
98
        </div>
99
100
        {#if result.confidence === 'insufficient'}
101
          <span class="hint">not enough shared digraph pairs for a reliable comparison — type more or use similar content</span>
102
        {/if}
103
104
        <span class="note">note: comparing prose vs code sessions for the same user will produce poor matches</span>
105
106
        {#if result.perDigraph.length > 0}
107
          <div class="breakdown-table-wrap">
108
            <table class="breakdown-table">
109
              <thead>
110
                <tr>
111
                  <th>keys</th>
112
                  <th>distance</th>
113
                  <th>match</th>
114
                  <th class="bar-col"></th>
115
                </tr>
116
              </thead>
117
              <tbody>
118
                {#each result.perDigraph as d}
119
                  <tr>
120
                    <td class="keys-cell">{d.normalizedKeys}</td>
121
                    <td class="distance-cell">{d.distance}</td>
122
                    <td class="match-cell" style="color: {matchColor(d.matchPercent)}">{d.matchPercent}%</td>
123
                    <td class="bar-cell">
124
                      <div class="bar-track">
125
                        <div
126
                          class="bar-fill"
127
                          style="width: {d.matchPercent}%; background: {matchColor(d.matchPercent)}"
128
                        ></div>
129
                      </div>
130
                    </td>
131
                  </tr>
132
                {/each}
133
              </tbody>
134
            </table>
135
          </div>
136
        {/if}
137
      </div>
138
    {/if}
139
  </div>
140
{/if}
141
142
<style>
143
  .empty {
144
    display: flex;
145
    flex-direction: column;
146
    align-items: center;
147
    justify-content: center;
148
    padding: 4rem 2rem;
149
    border: 1px dashed #333;
150
    gap: 0.5rem;
151
  }
152
153
  .empty p {
154
    font-size: 14px;
155
    color: #888;
156
  }
157
158
  .empty-sub {
159
    font-size: 11px;
160
    color: #555;
161
  }
162
163
  .comparison {
164
    display: flex;
165
    flex-direction: column;
166
    gap: 1rem;
167
  }
168
169
  .select-row {
170
    display: flex;
171
    gap: 0.5rem;
172
  }
173
174
  .profile-select {
175
    flex: 1;
176
    font-size: 12px;
177
    padding: 6px 8px;
178
    background: #121113;
179
    color: #ffffff;
180
    border: 1px solid white;
181
    font-family: "Commit Mono", monospace, sans-serif;
182
    cursor: pointer;
183
  }
184
185
  .profile-select option {
186
    background: #121113;
187
    color: #ffffff;
188
  }
189
190
  .compare-btn {
191
    font-size: 12px;
192
    padding: 6px 12px;
193
    white-space: nowrap;
194
  }
195
196
  .compare-btn:disabled {
197
    opacity: 0.3;
198
    cursor: not-allowed;
199
  }
200
201
  .hint {
202
    font-size: 11px;
203
    color: #555;
204
  }
205
206
  .note {
207
    font-size: 10px;
208
    color: #555;
209
    font-style: italic;
210
  }
211
212
  .results {
213
    display: flex;
214
    flex-direction: column;
215
    gap: 1rem;
216
  }
217
218
  .score-card {
219
    display: flex;
220
    border: 1px solid #333;
221
  }
222
223
  .similarity {
224
    display: flex;
225
    flex-direction: column;
226
    align-items: center;
227
    justify-content: center;
228
    padding: 1.5rem 2rem;
229
    border-right: 1px solid #333;
230
    min-width: 140px;
231
  }
232
233
  .similarity-value {
234
    font-size: 36px;
235
    font-weight: 700;
236
    line-height: 1;
237
  }
238
239
  .similarity-label {
240
    font-size: 10px;
241
    color: #888;
242
    margin-top: 0.35rem;
243
    text-transform: uppercase;
244
    letter-spacing: 0.05em;
245
  }
246
247
  .score-details {
248
    display: flex;
249
    flex-direction: column;
250
    flex: 1;
251
    padding: 0.75rem 1rem;
252
    gap: 0.35rem;
253
    justify-content: center;
254
  }
255
256
  .detail-row {
257
    display: flex;
258
    justify-content: space-between;
259
    align-items: center;
260
  }
261
262
  .detail-label {
263
    font-size: 11px;
264
    color: #888;
265
  }
266
267
  .detail-value {
268
    font-size: 13px;
269
    font-weight: 700;
270
  }
271
272
  .confidence-badge {
273
    text-transform: uppercase;
274
    font-size: 11px;
275
    letter-spacing: 0.05em;
276
  }
277
278
  .breakdown-table-wrap {
279
    border: 1px solid #333;
280
    max-height: 400px;
281
    overflow-y: auto;
282
    overflow-x: auto;
283
  }
284
285
  .breakdown-table {
286
    width: 100%;
287
    border-collapse: collapse;
288
    font-size: 12px;
289
  }
290
291
  .breakdown-table th,
292
  .breakdown-table td {
293
    padding: 0.4rem 0.5rem;
294
    text-align: center;
295
    border-bottom: 1px solid #333;
296
    white-space: nowrap;
297
  }
298
299
  .breakdown-table th {
300
    position: sticky;
301
    top: 0;
302
    background: #121113;
303
    color: #888;
304
    font-weight: 400;
305
    font-size: 10px;
306
    text-transform: uppercase;
307
    letter-spacing: 0.05em;
308
  }
309
310
  .breakdown-table .keys-cell {
311
    text-align: left;
312
    font-weight: 700;
313
  }
314
315
  .breakdown-table .distance-cell {
316
    color: #888;
317
  }
318
319
  .bar-col {
320
    width: 40%;
321
  }
322
323
  .bar-cell {
324
    padding: 0.4rem 0.75rem;
325
  }
326
327
  .bar-track {
328
    width: 100%;
329
    height: 4px;
330
    background: #333;
331
  }
332
333
  .bar-fill {
334
    height: 100%;
335
    transition: width 0.3s ease;
336
  }
337
338
  @media (max-width: 480px) {
339
    .score-card {
340
      flex-direction: column;
341
    }
342
343
    .similarity {
344
      border-right: none;
345
      border-bottom: 1px solid #333;
346
      padding: 1rem;
347
    }
348
349
    .bar-col {
350
      display: none;
351
    }
352
353
    .bar-cell {
354
      display: none;
355
    }
356
  }
357
</style>