granola-toolkit 0.20.0 → 0.21.0
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.
- package/README.md +2 -1
- package/dist/cli.js +161 -8
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -208,7 +208,8 @@ The initial browser client includes:
|
|
|
208
208
|
- a searchable meeting list
|
|
209
209
|
- sort and updated-date filters
|
|
210
210
|
- quick open by meeting id or title
|
|
211
|
-
- a meeting
|
|
211
|
+
- a focused meeting workspace with notes, transcript, metadata, and raw tabs
|
|
212
|
+
- keyboard-first workspace switching with `1`-`4`, `[` and `]`
|
|
212
213
|
- app-state status from the shared core
|
|
213
214
|
- note and transcript export actions backed by the same local API
|
|
214
215
|
- stronger empty and error states for list/detail failures
|
package/dist/cli.js
CHANGED
|
@@ -2346,6 +2346,37 @@ function renderGranolaWebPage() {
|
|
|
2346
2346
|
width: min(440px, 100%);
|
|
2347
2347
|
}
|
|
2348
2348
|
|
|
2349
|
+
.workspace-tabs {
|
|
2350
|
+
display: flex;
|
|
2351
|
+
flex-wrap: wrap;
|
|
2352
|
+
align-items: center;
|
|
2353
|
+
gap: 10px;
|
|
2354
|
+
padding: 0 24px 18px;
|
|
2355
|
+
}
|
|
2356
|
+
|
|
2357
|
+
.workspace-tab {
|
|
2358
|
+
border: 1px solid var(--line);
|
|
2359
|
+
border-radius: 999px;
|
|
2360
|
+
padding: 10px 14px;
|
|
2361
|
+
background: rgba(255, 255, 255, 0.72);
|
|
2362
|
+
color: var(--muted);
|
|
2363
|
+
cursor: pointer;
|
|
2364
|
+
font: inherit;
|
|
2365
|
+
font-weight: 700;
|
|
2366
|
+
}
|
|
2367
|
+
|
|
2368
|
+
.workspace-tab[data-selected="true"] {
|
|
2369
|
+
background: var(--ink);
|
|
2370
|
+
color: white;
|
|
2371
|
+
border-color: var(--ink);
|
|
2372
|
+
}
|
|
2373
|
+
|
|
2374
|
+
.workspace-hint {
|
|
2375
|
+
color: var(--muted);
|
|
2376
|
+
font-size: 0.86rem;
|
|
2377
|
+
margin-left: auto;
|
|
2378
|
+
}
|
|
2379
|
+
|
|
2349
2380
|
.button {
|
|
2350
2381
|
border: 0;
|
|
2351
2382
|
border-radius: 999px;
|
|
@@ -2402,6 +2433,17 @@ function renderGranolaWebPage() {
|
|
|
2402
2433
|
overflow: auto;
|
|
2403
2434
|
}
|
|
2404
2435
|
|
|
2436
|
+
.workspace-grid {
|
|
2437
|
+
display: grid;
|
|
2438
|
+
grid-template-columns: minmax(240px, 320px) minmax(0, 1fr);
|
|
2439
|
+
gap: 18px;
|
|
2440
|
+
}
|
|
2441
|
+
|
|
2442
|
+
.workspace-sidebar,
|
|
2443
|
+
.workspace-main {
|
|
2444
|
+
margin-bottom: 0;
|
|
2445
|
+
}
|
|
2446
|
+
|
|
2405
2447
|
.detail-section {
|
|
2406
2448
|
margin-bottom: 20px;
|
|
2407
2449
|
padding: 20px;
|
|
@@ -2440,9 +2482,14 @@ function renderGranolaWebPage() {
|
|
|
2440
2482
|
}
|
|
2441
2483
|
|
|
2442
2484
|
.field-row--inline,
|
|
2443
|
-
.toolbar-form
|
|
2485
|
+
.toolbar-form,
|
|
2486
|
+
.workspace-grid {
|
|
2444
2487
|
grid-template-columns: 1fr;
|
|
2445
2488
|
}
|
|
2489
|
+
|
|
2490
|
+
.workspace-hint {
|
|
2491
|
+
margin-left: 0;
|
|
2492
|
+
}
|
|
2446
2493
|
}
|
|
2447
2494
|
</style>
|
|
2448
2495
|
</head>
|
|
@@ -2500,6 +2547,13 @@ function renderGranolaWebPage() {
|
|
|
2500
2547
|
</div>
|
|
2501
2548
|
<p>Initial beta web client. It speaks to the same local API that future TUI and attach flows will use.</p>
|
|
2502
2549
|
</section>
|
|
2550
|
+
<nav class="workspace-tabs">
|
|
2551
|
+
<button class="workspace-tab" data-workspace-tab="notes">Notes</button>
|
|
2552
|
+
<button class="workspace-tab" data-workspace-tab="transcript">Transcript</button>
|
|
2553
|
+
<button class="workspace-tab" data-workspace-tab="metadata">Metadata</button>
|
|
2554
|
+
<button class="workspace-tab" data-workspace-tab="raw">Raw</button>
|
|
2555
|
+
<span class="workspace-hint">1-4 switch tabs, [ and ] cycle</span>
|
|
2556
|
+
</nav>
|
|
2503
2557
|
<div class="detail-meta" data-detail-meta></div>
|
|
2504
2558
|
<div class="detail-body" data-detail-body>
|
|
2505
2559
|
<div class="empty" data-empty>Select a meeting to inspect its notes and transcript.</div>
|
|
@@ -2516,10 +2570,12 @@ const state = {
|
|
|
2516
2570
|
quickOpen: "",
|
|
2517
2571
|
search: "",
|
|
2518
2572
|
selectedMeeting: null,
|
|
2573
|
+
selectedMeetingBundle: null,
|
|
2519
2574
|
selectedMeetingId: null,
|
|
2520
2575
|
sort: "updated-desc",
|
|
2521
2576
|
updatedFrom: "",
|
|
2522
2577
|
updatedTo: "",
|
|
2578
|
+
workspaceTab: "notes",
|
|
2523
2579
|
};
|
|
2524
2580
|
|
|
2525
2581
|
const els = {
|
|
@@ -2538,6 +2594,7 @@ const els = {
|
|
|
2538
2594
|
transcriptButton: document.querySelector("[data-export-transcripts]"),
|
|
2539
2595
|
updatedFrom: document.querySelector("[data-updated-from]"),
|
|
2540
2596
|
updatedTo: document.querySelector("[data-updated-to]"),
|
|
2597
|
+
workspaceTabs: document.querySelectorAll("[data-workspace-tab]"),
|
|
2541
2598
|
};
|
|
2542
2599
|
|
|
2543
2600
|
function escapeHtml(value) {
|
|
@@ -2579,6 +2636,12 @@ function currentFilterSummary() {
|
|
|
2579
2636
|
return parts.join(", ");
|
|
2580
2637
|
}
|
|
2581
2638
|
|
|
2639
|
+
function renderWorkspaceTabs() {
|
|
2640
|
+
for (const button of els.workspaceTabs) {
|
|
2641
|
+
button.dataset.selected = button.dataset.workspaceTab === state.workspaceTab ? "true" : "false";
|
|
2642
|
+
}
|
|
2643
|
+
}
|
|
2644
|
+
|
|
2582
2645
|
function renderAppState() {
|
|
2583
2646
|
if (!state.appState) {
|
|
2584
2647
|
els.appState.innerHTML = "<p>Waiting for server state…</p>";
|
|
@@ -2615,6 +2678,7 @@ function renderMeetingList() {
|
|
|
2615
2678
|
if (state.meetings.length === 0) {
|
|
2616
2679
|
state.selectedMeetingId = null;
|
|
2617
2680
|
state.selectedMeeting = null;
|
|
2681
|
+
state.selectedMeetingBundle = null;
|
|
2618
2682
|
const filterSummary = currentFilterSummary();
|
|
2619
2683
|
const message = filterSummary
|
|
2620
2684
|
? "No meetings match " + filterSummary + "."
|
|
@@ -2645,6 +2709,8 @@ function renderMeetingList() {
|
|
|
2645
2709
|
}
|
|
2646
2710
|
|
|
2647
2711
|
function renderMeetingDetail() {
|
|
2712
|
+
renderWorkspaceTabs();
|
|
2713
|
+
|
|
2648
2714
|
if (state.detailError) {
|
|
2649
2715
|
els.empty.hidden = false;
|
|
2650
2716
|
els.empty.textContent = state.detailError;
|
|
@@ -2669,15 +2735,46 @@ function renderMeetingDetail() {
|
|
|
2669
2735
|
'<div class="detail-chip">Transcript: ' + escapeHtml(String(record.meeting.transcriptSegmentCount)) + " segments</div>",
|
|
2670
2736
|
].join("");
|
|
2671
2737
|
|
|
2738
|
+
const bundle = state.selectedMeetingBundle;
|
|
2739
|
+
const metadataLines = [
|
|
2740
|
+
"Title: " + (record.meeting.title || record.meeting.id),
|
|
2741
|
+
"Created: " + record.meeting.createdAt,
|
|
2742
|
+
"Updated: " + record.meeting.updatedAt,
|
|
2743
|
+
"Tags: " + (record.meeting.tags.length ? record.meeting.tags.join(", ") : "none"),
|
|
2744
|
+
"Transcript loaded: " + (record.meeting.transcriptLoaded ? "yes" : "no"),
|
|
2745
|
+
].join("\n");
|
|
2746
|
+
|
|
2747
|
+
let mainTitle = "Notes";
|
|
2748
|
+
let mainBody = record.noteMarkdown || "";
|
|
2749
|
+
|
|
2750
|
+
switch (state.workspaceTab) {
|
|
2751
|
+
case "transcript":
|
|
2752
|
+
mainTitle = "Transcript";
|
|
2753
|
+
mainBody = record.transcriptText || "(Transcript unavailable)";
|
|
2754
|
+
break;
|
|
2755
|
+
case "metadata":
|
|
2756
|
+
mainTitle = "Metadata";
|
|
2757
|
+
mainBody = metadataLines;
|
|
2758
|
+
break;
|
|
2759
|
+
case "raw":
|
|
2760
|
+
mainTitle = "Raw Bundle";
|
|
2761
|
+
mainBody = JSON.stringify(bundle || record, null, 2);
|
|
2762
|
+
break;
|
|
2763
|
+
default:
|
|
2764
|
+
break;
|
|
2765
|
+
}
|
|
2766
|
+
|
|
2672
2767
|
els.detailBody.innerHTML = [
|
|
2673
|
-
'<
|
|
2674
|
-
"
|
|
2675
|
-
|
|
2676
|
-
"</
|
|
2677
|
-
|
|
2678
|
-
"
|
|
2679
|
-
|
|
2768
|
+
'<div class="workspace-grid">',
|
|
2769
|
+
'<aside class="detail-section workspace-sidebar">',
|
|
2770
|
+
"<h2>Meeting Metadata</h2>",
|
|
2771
|
+
'<pre class="detail-pre">' + escapeHtml(metadataLines) + "</pre>",
|
|
2772
|
+
"</aside>",
|
|
2773
|
+
'<section class="detail-section workspace-main">',
|
|
2774
|
+
"<h2>" + escapeHtml(mainTitle) + "</h2>",
|
|
2775
|
+
'<pre class="detail-pre">' + escapeHtml(mainBody) + "</pre>",
|
|
2680
2776
|
"</section>",
|
|
2777
|
+
"</div>",
|
|
2681
2778
|
].join("");
|
|
2682
2779
|
}
|
|
2683
2780
|
|
|
@@ -2733,6 +2830,7 @@ async function loadMeetings(options = {}) {
|
|
|
2733
2830
|
} catch (error) {
|
|
2734
2831
|
state.listError = error instanceof Error ? error.message : String(error);
|
|
2735
2832
|
state.selectedMeeting = null;
|
|
2833
|
+
state.selectedMeetingBundle = null;
|
|
2736
2834
|
state.detailError = state.listError;
|
|
2737
2835
|
renderMeetingList();
|
|
2738
2836
|
renderMeetingDetail();
|
|
@@ -2746,10 +2844,12 @@ async function loadMeeting(id) {
|
|
|
2746
2844
|
try {
|
|
2747
2845
|
state.detailError = "";
|
|
2748
2846
|
const payload = await fetchJson("/meetings/" + encodeURIComponent(id));
|
|
2847
|
+
state.selectedMeetingBundle = payload;
|
|
2749
2848
|
state.selectedMeeting = payload.meeting || null;
|
|
2750
2849
|
renderMeetingDetail();
|
|
2751
2850
|
} catch (error) {
|
|
2752
2851
|
state.selectedMeeting = null;
|
|
2852
|
+
state.selectedMeetingBundle = null;
|
|
2753
2853
|
state.detailError = error instanceof Error ? error.message : String(error);
|
|
2754
2854
|
renderMeetingDetail();
|
|
2755
2855
|
}
|
|
@@ -2891,6 +2991,59 @@ els.quickOpenButton.addEventListener("click", () => {
|
|
|
2891
2991
|
void quickOpenMeeting();
|
|
2892
2992
|
});
|
|
2893
2993
|
|
|
2994
|
+
els.workspaceTabs.forEach((button) => {
|
|
2995
|
+
button.addEventListener("click", () => {
|
|
2996
|
+
state.workspaceTab = button.dataset.workspaceTab || "notes";
|
|
2997
|
+
renderMeetingDetail();
|
|
2998
|
+
});
|
|
2999
|
+
});
|
|
3000
|
+
|
|
3001
|
+
document.addEventListener("keydown", (event) => {
|
|
3002
|
+
if (
|
|
3003
|
+
event.target instanceof HTMLInputElement ||
|
|
3004
|
+
event.target instanceof HTMLSelectElement ||
|
|
3005
|
+
event.target instanceof HTMLTextAreaElement
|
|
3006
|
+
) {
|
|
3007
|
+
return;
|
|
3008
|
+
}
|
|
3009
|
+
|
|
3010
|
+
const tabs = ["notes", "transcript", "metadata", "raw"];
|
|
3011
|
+
if (event.key === "1") {
|
|
3012
|
+
state.workspaceTab = "notes";
|
|
3013
|
+
renderMeetingDetail();
|
|
3014
|
+
return;
|
|
3015
|
+
}
|
|
3016
|
+
|
|
3017
|
+
if (event.key === "2") {
|
|
3018
|
+
state.workspaceTab = "transcript";
|
|
3019
|
+
renderMeetingDetail();
|
|
3020
|
+
return;
|
|
3021
|
+
}
|
|
3022
|
+
|
|
3023
|
+
if (event.key === "3") {
|
|
3024
|
+
state.workspaceTab = "metadata";
|
|
3025
|
+
renderMeetingDetail();
|
|
3026
|
+
return;
|
|
3027
|
+
}
|
|
3028
|
+
|
|
3029
|
+
if (event.key === "4") {
|
|
3030
|
+
state.workspaceTab = "raw";
|
|
3031
|
+
renderMeetingDetail();
|
|
3032
|
+
return;
|
|
3033
|
+
}
|
|
3034
|
+
|
|
3035
|
+
const currentIndex = tabs.indexOf(state.workspaceTab);
|
|
3036
|
+
if (event.key === "]") {
|
|
3037
|
+
state.workspaceTab = tabs[(currentIndex + 1) % tabs.length];
|
|
3038
|
+
renderMeetingDetail();
|
|
3039
|
+
}
|
|
3040
|
+
|
|
3041
|
+
if (event.key === "[") {
|
|
3042
|
+
state.workspaceTab = tabs[(currentIndex + tabs.length - 1) % tabs.length];
|
|
3043
|
+
renderMeetingDetail();
|
|
3044
|
+
}
|
|
3045
|
+
});
|
|
3046
|
+
|
|
2894
3047
|
const events = new EventSource("/events");
|
|
2895
3048
|
events.addEventListener("state.updated", (event) => {
|
|
2896
3049
|
const payload = JSON.parse(event.data);
|