@dualityinstitute/blink 0.0.3 → 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -27,6 +27,13 @@ var CHANNEL_PRESENTER_DISMISS = "blink:presenter-dismiss";
27
27
  var CHANNEL_PRESENTER_SET_PRIVACY = "blink:presenter-set-privacy";
28
28
  var CHANNEL_PRESENTER_PRIVACY_CHANGED = "blink:presenter-privacy-changed";
29
29
  var CHANNEL_PRESENTER_SCROLL = "blink:presenter-scroll";
30
+ var CHANNEL_SETTINGS_LOAD_PROMPTS = "blink:settings-load-prompts";
31
+ var CHANNEL_SETTINGS_SAVE_PROMPTS = "blink:settings-save-prompts";
32
+ var CHANNEL_SETTINGS_LOAD_ANSWERS = "blink:settings-load-answers";
33
+ var CHANNEL_SETTINGS_DISMISS = "blink:settings-dismiss";
34
+ var CHANNEL_SETTINGS_LOAD_TRANSCRIPTS = "blink:settings-load-transcripts";
35
+ var CHANNEL_SETTINGS_READ_TRANSCRIPT = "blink:settings-read-transcript";
36
+ var CHANNEL_SETTINGS_SHOW_TAB = "blink:settings-show-tab";
30
37
 
31
38
  // src/electron/preload.ts
32
39
  function subscribe(channel, callback) {
@@ -111,5 +118,29 @@ var presenterAPI = {
111
118
  subscribe(CHANNEL_PRESENTER_SCROLL, callback);
112
119
  }
113
120
  };
121
+ var settingsAPI = {
122
+ loadPrompts() {
123
+ return import_electron.ipcRenderer.invoke(CHANNEL_SETTINGS_LOAD_PROMPTS);
124
+ },
125
+ savePrompts(prompts) {
126
+ return import_electron.ipcRenderer.invoke(CHANNEL_SETTINGS_SAVE_PROMPTS, prompts);
127
+ },
128
+ loadAnswers() {
129
+ return import_electron.ipcRenderer.invoke(CHANNEL_SETTINGS_LOAD_ANSWERS);
130
+ },
131
+ loadTranscripts(query) {
132
+ return import_electron.ipcRenderer.invoke(CHANNEL_SETTINGS_LOAD_TRANSCRIPTS, query);
133
+ },
134
+ readTranscript(file) {
135
+ return import_electron.ipcRenderer.invoke(CHANNEL_SETTINGS_READ_TRANSCRIPT, file);
136
+ },
137
+ dismiss() {
138
+ import_electron.ipcRenderer.send(CHANNEL_SETTINGS_DISMISS);
139
+ },
140
+ onShowTab(callback) {
141
+ subscribe(CHANNEL_SETTINGS_SHOW_TAB, callback);
142
+ }
143
+ };
114
144
  import_electron.contextBridge.exposeInMainWorld("blink", blinkAPI);
115
145
  import_electron.contextBridge.exposeInMainWorld("presenter", presenterAPI);
146
+ import_electron.contextBridge.exposeInMainWorld("settings", settingsAPI);
@@ -215,6 +215,23 @@ body {
215
215
  border-color: color-mix(in srgb, var(--fg) 24%, transparent);
216
216
  }
217
217
 
218
+ .meeting-control.is-live {
219
+ color: #fff;
220
+ background: var(--leave-bg, #c0392b);
221
+ border-color: color-mix(in srgb, #c0392b 70%, #000);
222
+ cursor: pointer;
223
+ font-variant-numeric: tabular-nums;
224
+ }
225
+
226
+ .meeting-control.is-live:hover {
227
+ background: color-mix(in srgb, #c0392b 85%, #fff);
228
+ }
229
+
230
+ .meeting-control.is-live .meeting-indicator {
231
+ background: #fff;
232
+ border-color: #fff;
233
+ }
234
+
218
235
  .meeting-control.is-meeting .meeting-indicator {
219
236
  background: currentColor;
220
237
  }
@@ -242,6 +259,46 @@ body {
242
259
  outline-offset: 2px;
243
260
  }
244
261
 
262
+ .runtime-error {
263
+ display: flex;
264
+ align-items: flex-start;
265
+ gap: 8px;
266
+ margin: 6px 12px 0 12px;
267
+ padding: 8px 10px;
268
+ border: 1px solid rgba(200, 60, 60, 0.35);
269
+ border-radius: 8px;
270
+ background: rgba(200, 60, 60, 0.12);
271
+ font-size: 12px;
272
+ line-height: 1.4;
273
+ }
274
+
275
+ .runtime-error[hidden] {
276
+ display: none;
277
+ }
278
+
279
+ .runtime-error-text {
280
+ flex: 1;
281
+ min-width: 0;
282
+ overflow-wrap: anywhere;
283
+ color: #ffd9d9;
284
+ }
285
+
286
+ .runtime-error-close {
287
+ flex: none;
288
+ padding: 0 4px;
289
+ border: 0;
290
+ background: transparent;
291
+ color: #ffd9d9;
292
+ font-size: 14px;
293
+ line-height: 1;
294
+ cursor: pointer;
295
+ opacity: 0.75;
296
+ }
297
+
298
+ .runtime-error-close:hover {
299
+ opacity: 1;
300
+ }
301
+
245
302
  .response-context {
246
303
  display: flex;
247
304
  align-items: center;
@@ -395,35 +452,90 @@ body {
395
452
  opacity: 0.45;
396
453
  }
397
454
 
455
+ .workspace {
456
+ display: flex;
457
+ flex: 1 1 auto;
458
+ min-height: 0;
459
+ }
460
+
398
461
  .transcript-panel {
399
- flex: 0 0 auto;
400
- max-height: 74px;
401
- overflow-y: auto;
402
- padding: 6px 14px 7px 16px;
403
- border-bottom: 1px solid var(--divider);
462
+ display: flex;
463
+ flex-direction: column;
464
+ /* Shrinks below its 400px basis only when the window is too narrow to hold
465
+ both panes, so dragging the popup small never hides the answer entirely. */
466
+ flex: 0 1 400px;
467
+ width: 400px;
468
+ min-width: 200px;
469
+ border-right: 1px solid var(--divider);
404
470
  background: color-mix(in srgb, var(--bg-strong) 12%, transparent);
405
471
  font-size: 11.5px;
406
472
  line-height: 1.35;
473
+ }
474
+
475
+ .transcript-panel[hidden] {
476
+ display: none;
477
+ }
478
+
479
+ .transcript-heading {
480
+ display: flex;
481
+ align-items: baseline;
482
+ justify-content: space-between;
483
+ gap: 8px;
484
+ padding: 7px 12px 6px 16px;
485
+ border-bottom: 1px solid var(--divider);
486
+ }
487
+
488
+ .transcript-heading-label {
489
+ font-size: 9.5px;
490
+ font-weight: 700;
491
+ letter-spacing: 0.06em;
492
+ text-transform: uppercase;
493
+ color: var(--fg-subtle);
494
+ }
495
+
496
+ .transcript-count {
497
+ font-size: 9.5px;
498
+ font-variant-numeric: tabular-nums;
499
+ color: var(--fg-subtle);
500
+ }
501
+
502
+ .transcript-scroll {
503
+ flex: 1 1 auto;
504
+ min-height: 0;
505
+ overflow-y: auto;
506
+ padding: 8px 12px 10px 16px;
407
507
  user-select: text;
408
508
  -webkit-user-select: text;
409
509
  scrollbar-width: thin;
410
510
  scrollbar-color: var(--fg-subtle) transparent;
411
511
  }
412
512
 
413
- .transcript-panel::-webkit-scrollbar {
513
+ .transcript-scroll::-webkit-scrollbar {
414
514
  width: 5px;
415
515
  }
416
516
 
417
- .transcript-panel::-webkit-scrollbar-thumb {
517
+ .transcript-scroll::-webkit-scrollbar-thumb {
418
518
  background: var(--fg-subtle);
419
519
  border-radius: 3px;
420
520
  opacity: 0.4;
421
521
  }
422
522
 
523
+ /* Placeholder lives in CSS so an empty transcript needs no throwaway DOM node. */
524
+ .transcript-lines.is-empty::before {
525
+ content: "Waiting for speech…";
526
+ color: var(--fg-subtle);
527
+ font-style: italic;
528
+ }
529
+
423
530
  .transcript-line {
424
531
  display: flex;
425
- gap: 6px;
426
- margin: 1px 0;
532
+ flex-direction: column;
533
+ gap: 1px;
534
+ margin: 0 0 7px;
535
+ }
536
+
537
+ .transcript-line.is-partial .transcript-text {
538
+ color: var(--fg-subtle);
427
539
  }
428
540
 
429
541
  .transcript-speaker {
@@ -451,11 +563,6 @@ body {
451
563
  overflow-wrap: break-word;
452
564
  }
453
565
 
454
- .transcript-empty {
455
- color: var(--fg-subtle);
456
- font-style: italic;
457
- }
458
-
459
566
  .pill.reconciled-pill {
460
567
  background: var(--pill-reconciled-bg);
461
568
  color: var(--pill-reconciled-fg);
@@ -543,8 +650,11 @@ body {
543
650
 
544
651
  .card-body {
545
652
  position: relative;
546
- flex: 1 1 auto;
653
+ /* The floor is what forces a narrowed window to shrink the transcript column
654
+ first; without it the answer silently collapses to an unusable sliver. */
655
+ flex: 1 1 0;
547
656
  min-height: 0;
657
+ min-width: 280px;
548
658
  overflow-y: auto;
549
659
  padding: 12px 14px 14px 14px;
550
660
  scrollbar-width: thin;
@@ -68,6 +68,11 @@
68
68
  </div>
69
69
  </header>
70
70
 
71
+ <div class="runtime-error" id="runtime-error" hidden role="alert">
72
+ <span class="runtime-error-text" id="runtime-error-text"></span>
73
+ <button type="button" class="runtime-error-close" id="runtime-error-close" aria-label="Dismiss">×</button>
74
+ </div>
75
+
71
76
  <div class="response-context">
72
77
  <div class="source" id="source">
73
78
  <span class="source-label">Ready for a screenshot</span>
@@ -108,14 +113,22 @@
108
113
  >Ask</button>
109
114
  </div>
110
115
 
111
- <div
116
+ <div class="workspace" id="workspace">
117
+ <aside
112
118
  class="transcript-panel"
113
119
  id="transcript-panel"
114
120
  aria-label="Live meeting transcript"
115
121
  hidden
116
122
  >
117
- <div class="transcript-lines" id="transcript-lines"></div>
118
- </div>
123
+ <div class="transcript-heading">
124
+ <span class="transcript-heading-label">Live transcript</span>
125
+ <span class="transcript-count" id="transcript-count"></span>
126
+ </div>
127
+ <div class="transcript-scroll" id="transcript-scroll">
128
+ <div class="transcript-lines" id="transcript-lines"></div>
129
+ <div class="transcript-volatile" id="transcript-volatile"></div>
130
+ </div>
131
+ </aside>
119
132
 
120
133
  <section class="card-body" id="body">
121
134
  <div class="answering" id="answering" hidden>
@@ -135,6 +148,7 @@
135
148
  <div class="error-message" id="error-message"></div>
136
149
  </div>
137
150
  </section>
151
+ </div>
138
152
  </main>
139
153
  <script src="./popup.js"></script>
140
154
  </body>
@@ -3736,10 +3736,9 @@ var state = {
3736
3736
  meeting: false,
3737
3737
  meetingStarting: false,
3738
3738
  meetingAsking: false,
3739
- transcriptAvailable: false,
3740
3739
  screenshotCount: 0,
3741
3740
  showTranscript: true,
3742
- transcriptLines: []
3741
+ meetingStartedAt: null
3743
3742
  };
3744
3743
  function el(id) {
3745
3744
  const node = document.getElementById(id);
@@ -3762,7 +3761,10 @@ var dom = {
3762
3761
  askMeeting: el("ask-meeting"),
3763
3762
  transcriptToggle: el("transcript-toggle"),
3764
3763
  transcriptPanel: el("transcript-panel"),
3764
+ transcriptScroll: el("transcript-scroll"),
3765
3765
  transcriptLines: el("transcript-lines"),
3766
+ transcriptVolatile: el("transcript-volatile"),
3767
+ transcriptCount: el("transcript-count"),
3766
3768
  privacy: el("privacy"),
3767
3769
  privacyLabel: el("privacy-label"),
3768
3770
  usagePill: el("usage-pill"),
@@ -3775,6 +3777,9 @@ var dom = {
3775
3777
  answer: el("answer"),
3776
3778
  error: el("error"),
3777
3779
  errorMessage: el("error-message"),
3780
+ runtimeError: el("runtime-error"),
3781
+ runtimeErrorText: el("runtime-error-text"),
3782
+ runtimeErrorClose: el("runtime-error-close"),
3778
3783
  body: el("body")
3779
3784
  };
3780
3785
  function setState(next) {
@@ -3823,73 +3828,134 @@ function updateMonitoring(payload) {
3823
3828
  dom.monitoringControl.classList.toggle("is-starting", payload.monitoringStarting);
3824
3829
  dom.monitoringControl.setAttribute("aria-busy", payload.monitoringStarting ? "true" : "false");
3825
3830
  }
3831
+ var transcriptRows = new Map;
3826
3832
  function renderTranscript() {
3827
3833
  const expanded = state.showTranscript;
3828
3834
  dom.transcriptToggle.setAttribute("aria-expanded", expanded ? "true" : "false");
3829
- dom.transcriptPanel.hidden = !state.meeting || !expanded;
3830
- if (dom.transcriptPanel.hidden)
3831
- return;
3832
- if (state.transcriptLines.length === 0) {
3833
- const empty = document.createElement("div");
3834
- empty.className = "transcript-empty";
3835
- empty.textContent = "Waiting for speech…";
3836
- dom.transcriptLines.replaceChildren(empty);
3837
- return;
3838
- }
3839
- const rows = state.transcriptLines.map((line) => {
3840
- const row = document.createElement("div");
3841
- row.className = "transcript-line";
3842
- const speaker = document.createElement("span");
3843
- speaker.className = `transcript-speaker is-${line.speaker}`;
3844
- speaker.textContent = line.speaker === "them" ? "Them" : "You";
3845
- const text2 = document.createElement("span");
3846
- text2.className = "transcript-text";
3847
- text2.textContent = line.text;
3848
- row.append(speaker, text2);
3849
- return row;
3850
- });
3851
- dom.transcriptLines.replaceChildren(...rows);
3852
- pinTranscriptToBottom();
3835
+ const visible = state.meeting && expanded;
3836
+ dom.transcriptPanel.hidden = !visible;
3837
+ dom.root.classList.toggle("has-transcript", visible);
3838
+ updateTranscriptPlaceholder();
3839
+ }
3840
+ function buildTranscriptRow(line) {
3841
+ const row = document.createElement("div");
3842
+ row.className = "transcript-line";
3843
+ const speaker = document.createElement("span");
3844
+ speaker.className = `transcript-speaker is-${line.speaker}`;
3845
+ speaker.textContent = line.speaker === "them" ? "Them" : "You";
3846
+ const text2 = document.createElement("span");
3847
+ text2.className = "transcript-text";
3848
+ text2.textContent = line.text;
3849
+ row.append(speaker, text2);
3850
+ return row;
3851
+ }
3852
+ function setRowText(row, text2) {
3853
+ const node = row.querySelector(".transcript-text");
3854
+ if (node !== null)
3855
+ node.textContent = text2;
3856
+ }
3857
+ function updateTranscriptPlaceholder() {
3858
+ const empty = transcriptRows.size === 0 && dom.transcriptVolatile.childElementCount === 0;
3859
+ dom.transcriptLines.classList.toggle("is-empty", empty);
3860
+ dom.transcriptCount.textContent = transcriptRows.size === 0 ? "" : String(transcriptRows.size);
3861
+ }
3862
+ function isTranscriptAtBottom() {
3863
+ const node = dom.transcriptScroll;
3864
+ return node.scrollHeight - node.scrollTop - node.clientHeight < 40;
3853
3865
  }
3854
3866
  function pinTranscriptToBottom() {
3855
- dom.transcriptPanel.scrollTop = dom.transcriptPanel.scrollHeight;
3867
+ dom.transcriptScroll.scrollTop = dom.transcriptScroll.scrollHeight;
3856
3868
  }
3857
3869
  function onTranscriptUpdate(payload) {
3858
- state.transcriptLines = Array.isArray(payload.lines) ? payload.lines : [];
3859
- renderTranscript();
3870
+ if (payload.reset === true) {
3871
+ transcriptRows.clear();
3872
+ dom.transcriptLines.replaceChildren();
3873
+ dom.transcriptVolatile.replaceChildren();
3874
+ }
3875
+ const follow = isTranscriptAtBottom();
3876
+ for (const line of payload.changed ?? []) {
3877
+ const existing = transcriptRows.get(line.id);
3878
+ if (existing !== undefined) {
3879
+ setRowText(existing, line.text);
3880
+ continue;
3881
+ }
3882
+ const row = buildTranscriptRow(line);
3883
+ transcriptRows.set(line.id, row);
3884
+ dom.transcriptLines.append(row);
3885
+ }
3886
+ dom.transcriptVolatile.replaceChildren(...(payload.volatile ?? []).map((line) => {
3887
+ const row = buildTranscriptRow(line);
3888
+ row.classList.add("is-partial");
3889
+ return row;
3890
+ }));
3891
+ updateTranscriptPlaceholder();
3892
+ if (follow)
3893
+ pinTranscriptToBottom();
3894
+ }
3895
+ function clearMeetingAnswer() {
3896
+ const label = document.createElement("span");
3897
+ label.className = "source-label";
3898
+ label.textContent = "Ready for a screenshot";
3899
+ dom.source.replaceChildren(label);
3900
+ delete dom.source.dataset["source"];
3901
+ dom.answer.replaceChildren();
3902
+ dom.errorMessage.textContent = "";
3903
+ state.streamBuffer = "";
3904
+ setReconciled(false);
3905
+ setStopped(false);
3906
+ setState("idle");
3860
3907
  }
3861
3908
  function updateMeeting(payload) {
3909
+ const wasMeeting = state.meeting;
3862
3910
  state.meeting = payload.meeting;
3863
3911
  state.meetingStarting = payload.meetingStarting;
3864
3912
  state.meetingAsking = payload.asking;
3865
- state.transcriptAvailable = payload.transcriptAvailable;
3866
3913
  state.screenshotCount = Math.max(0, Math.floor(payload.screenshotCount));
3867
3914
  state.showTranscript = payload.showTranscript;
3868
3915
  renderTranscript();
3916
+ state.meetingStartedAt = payload.startedAt ?? null;
3869
3917
  const active = payload.meeting || payload.meetingStarting;
3870
3918
  dom.meeting.checked = active;
3871
3919
  dom.meeting.disabled = payload.meetingStarting;
3872
- dom.meetingLabel.textContent = payload.meetingStarting ? "Starting…" : "Meeting";
3873
3920
  dom.meetingControl.classList.toggle("is-meeting", active);
3921
+ dom.meetingControl.classList.toggle("is-live", payload.meeting);
3922
+ dom.meetingControl.title = payload.meeting ? "Leave the meeting and stop recording" : "Capture private Meeting context";
3923
+ renderMeetingClock();
3874
3924
  dom.meetingControl.classList.toggle("is-starting", payload.meetingStarting);
3875
3925
  dom.meetingControl.setAttribute("aria-busy", payload.meetingStarting ? "true" : "false");
3876
3926
  renderMeetingContext();
3927
+ if (wasMeeting && !payload.meeting)
3928
+ clearMeetingAnswer();
3929
+ }
3930
+ function elapsedLabel(startedAt) {
3931
+ const total = Math.max(0, Math.floor((Date.now() - startedAt) / 1000));
3932
+ const hours = Math.floor(total / 3600);
3933
+ const minutes = Math.floor(total % 3600 / 60);
3934
+ const seconds = total % 60;
3935
+ const pad = (value) => String(value).padStart(2, "0");
3936
+ return hours > 0 ? `${hours}:${pad(minutes)}:${pad(seconds)}` : `${minutes}:${pad(seconds)}`;
3937
+ }
3938
+ function renderMeetingClock() {
3939
+ if (state.meetingStarting) {
3940
+ dom.meetingLabel.textContent = "Starting…";
3941
+ return;
3942
+ }
3943
+ if (!state.meeting || state.meetingStartedAt === null) {
3944
+ dom.meetingLabel.textContent = "Meeting";
3945
+ return;
3946
+ }
3947
+ dom.meetingLabel.textContent = `Leave · ${elapsedLabel(state.meetingStartedAt)}`;
3877
3948
  }
3949
+ window.setInterval(renderMeetingClock, 1000);
3878
3950
  function renderMeetingContext() {
3879
3951
  const visible = state.meeting || state.meetingStarting;
3880
- const hasContext = state.transcriptAvailable || state.screenshotCount > 0;
3881
3952
  const answerRunning = state.currentState === "answering" || state.currentState === "streaming";
3882
3953
  dom.meetingContext.hidden = !visible;
3883
3954
  dom.root.classList.toggle("has-meeting", visible);
3884
- const details = [];
3885
- if (state.transcriptAvailable)
3886
- details.push("Transcript ready");
3887
- if (state.screenshotCount > 0) {
3888
- details.push(`${state.screenshotCount} screenshot${state.screenshotCount === 1 ? "" : "s"}`);
3889
- }
3890
- dom.contextCopy.textContent = details.length > 0 ? details.join(" · ") : state.meetingStarting ? "Preparing private context…" : "No context yet";
3891
- dom.contextCopy.classList.toggle("has-context", hasContext);
3892
- dom.askMeeting.disabled = !state.meeting || state.meetingStarting || state.meetingAsking || answerRunning || !hasContext;
3955
+ const staged = state.screenshotCount;
3956
+ dom.contextCopy.textContent = state.meetingStarting ? "Preparing private context…" : staged > 0 ? `${staged} screenshot${staged === 1 ? "" : "s"}` : "Transcript only";
3957
+ dom.contextCopy.classList.toggle("has-context", staged > 0);
3958
+ dom.askMeeting.disabled = !state.meeting || state.meetingStarting || state.meetingAsking || answerRunning;
3893
3959
  dom.askMeeting.textContent = state.meetingAsking ? "Asking…" : "Ask";
3894
3960
  dom.askMeeting.setAttribute("aria-busy", state.meetingAsking ? "true" : "false");
3895
3961
  }
@@ -3910,11 +3976,22 @@ function updateUsage(payload) {
3910
3976
  dom.usagePill.classList.toggle("is-near-limit", usage.status === "active" && !exhausted && usage.answersUsed >= usage.answersLimit * 0.9);
3911
3977
  dom.usagePill.classList.toggle("is-exhausted", usage.status !== "active" || exhausted);
3912
3978
  }
3979
+ function updateRuntimeError(header) {
3980
+ const message = header.lastError;
3981
+ if (message === undefined || message === "") {
3982
+ dom.runtimeError.hidden = true;
3983
+ dom.runtimeErrorText.textContent = "";
3984
+ return;
3985
+ }
3986
+ dom.runtimeErrorText.textContent = message;
3987
+ dom.runtimeError.hidden = false;
3988
+ }
3913
3989
  function applyHeader(header) {
3914
3990
  updatePrivacy(header.hideFromCapture);
3915
3991
  updateMonitoring(header);
3916
3992
  updateMeeting(header);
3917
3993
  updateUsage(header);
3994
+ updateRuntimeError(header);
3918
3995
  }
3919
3996
  async function loadHeader() {
3920
3997
  if (window.blink === undefined)
@@ -4031,7 +4108,6 @@ dom.meeting.addEventListener("change", () => {
4031
4108
  meetingStarting: false,
4032
4109
  asking: state.meetingAsking,
4033
4110
  showTranscript: state.showTranscript,
4034
- transcriptAvailable: state.transcriptAvailable,
4035
4111
  screenshotCount: state.screenshotCount
4036
4112
  });
4037
4113
  window.blink?.setMeeting(meeting);
@@ -4056,6 +4132,9 @@ dom.transcriptToggle.addEventListener("click", () => {
4056
4132
  renderTranscript();
4057
4133
  window.blink?.setShowTranscript(state.showTranscript);
4058
4134
  });
4135
+ dom.runtimeErrorClose.addEventListener("click", () => {
4136
+ dom.runtimeError.hidden = true;
4137
+ });
4059
4138
  dom.abort.addEventListener("click", () => {
4060
4139
  if (window.blink === undefined || dom.abort.disabled)
4061
4140
  return;