granola-toolkit 0.31.0 → 0.32.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 +3 -0
- package/dist/cli.js +214 -11
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -275,10 +275,13 @@ You can deep-link into a specific meeting with either:
|
|
|
275
275
|
|
|
276
276
|
The initial browser client includes:
|
|
277
277
|
|
|
278
|
+
- a dedicated folder pane with an explicit All meetings scope
|
|
278
279
|
- a searchable meeting list
|
|
280
|
+
- folder-aware meeting browsing with one-click scope changes
|
|
279
281
|
- a fast local-index warm start for meeting browsing before live documents finish loading
|
|
280
282
|
- sort and updated-date filters
|
|
281
283
|
- quick open by meeting id or title
|
|
284
|
+
- browser URL state that preserves the selected folder, meeting, and tab
|
|
282
285
|
- a focused meeting workspace with notes, transcript, metadata, and raw tabs
|
|
283
286
|
- keyboard-first workspace switching with `1`-`4`, `[` and `]`
|
|
284
287
|
- app-state status from the shared core
|
package/dist/cli.js
CHANGED
|
@@ -4204,10 +4204,13 @@ const workspaceTabs = ["notes", "transcript", "metadata", "raw"];
|
|
|
4204
4204
|
const state = {
|
|
4205
4205
|
appState: null,
|
|
4206
4206
|
detailError: "",
|
|
4207
|
+
folderError: "",
|
|
4208
|
+
folders: [],
|
|
4207
4209
|
listError: "",
|
|
4208
4210
|
meetings: [],
|
|
4209
4211
|
quickOpen: "",
|
|
4210
4212
|
search: "",
|
|
4213
|
+
selectedFolderId: null,
|
|
4211
4214
|
selectedMeeting: null,
|
|
4212
4215
|
selectedMeetingBundle: null,
|
|
4213
4216
|
selectedMeetingId: null,
|
|
@@ -4225,6 +4228,7 @@ const els = {
|
|
|
4225
4228
|
detailBody: document.querySelector("[data-detail-body]"),
|
|
4226
4229
|
detailMeta: document.querySelector("[data-detail-meta]"),
|
|
4227
4230
|
empty: document.querySelector("[data-empty]"),
|
|
4231
|
+
folderList: document.querySelector("[data-folder-list]"),
|
|
4228
4232
|
jobsList: document.querySelector("[data-jobs-list]"),
|
|
4229
4233
|
list: document.querySelector("[data-meeting-list]"),
|
|
4230
4234
|
noteButton: document.querySelector("[data-export-notes]"),
|
|
@@ -4251,6 +4255,7 @@ function parseWorkspaceTab(value) {
|
|
|
4251
4255
|
function startupSelection() {
|
|
4252
4256
|
const params = new URLSearchParams(window.location.search);
|
|
4253
4257
|
return {
|
|
4258
|
+
folderId: params.get("folder")?.trim() || "",
|
|
4254
4259
|
meetingId: params.get("meeting")?.trim() || "",
|
|
4255
4260
|
workspaceTab: parseWorkspaceTab(params.get("tab")),
|
|
4256
4261
|
};
|
|
@@ -4259,6 +4264,12 @@ function startupSelection() {
|
|
|
4259
4264
|
function syncBrowserUrl() {
|
|
4260
4265
|
const url = new URL(window.location.href);
|
|
4261
4266
|
|
|
4267
|
+
if (state.selectedFolderId) {
|
|
4268
|
+
url.searchParams.set("folder", state.selectedFolderId);
|
|
4269
|
+
} else {
|
|
4270
|
+
url.searchParams.delete("folder");
|
|
4271
|
+
}
|
|
4272
|
+
|
|
4262
4273
|
if (state.selectedMeetingId) {
|
|
4263
4274
|
url.searchParams.set("meeting", state.selectedMeetingId);
|
|
4264
4275
|
} else {
|
|
@@ -4302,6 +4313,11 @@ function syncFilterInputs() {
|
|
|
4302
4313
|
function currentFilterSummary() {
|
|
4303
4314
|
const parts = [];
|
|
4304
4315
|
|
|
4316
|
+
if (state.selectedFolderId) {
|
|
4317
|
+
const folder = state.folders.find((candidate) => candidate.id === state.selectedFolderId);
|
|
4318
|
+
parts.push("folder " + (folder ? '"' + folder.name + '"' : '"' + state.selectedFolderId + '"'));
|
|
4319
|
+
}
|
|
4320
|
+
|
|
4305
4321
|
if (state.search) {
|
|
4306
4322
|
parts.push('search "' + state.search + '"');
|
|
4307
4323
|
}
|
|
@@ -4344,6 +4360,9 @@ function renderAppState() {
|
|
|
4344
4360
|
: appState.index.available
|
|
4345
4361
|
? "available"
|
|
4346
4362
|
: "not built";
|
|
4363
|
+
const folderStatus = appState.folders.loaded
|
|
4364
|
+
? appState.folders.count + " folders"
|
|
4365
|
+
: "not loaded";
|
|
4347
4366
|
|
|
4348
4367
|
els.appState.innerHTML = [
|
|
4349
4368
|
'<div class="status-grid">',
|
|
@@ -4351,6 +4370,7 @@ function renderAppState() {
|
|
|
4351
4370
|
'<div><span class="status-label">View</span><strong>' + escapeHtml(appState.ui.view) + "</strong></div>",
|
|
4352
4371
|
'<div><span class="status-label">Auth</span><strong>' + escapeHtml(authMode) + "</strong></div>",
|
|
4353
4372
|
'<div><span class="status-label">Documents</span><strong>' + escapeHtml(docs) + "</strong></div>",
|
|
4373
|
+
'<div><span class="status-label">Folders</span><strong>' + escapeHtml(folderStatus) + "</strong></div>",
|
|
4354
4374
|
'<div><span class="status-label">Cache</span><strong>' + escapeHtml(cache) + "</strong></div>",
|
|
4355
4375
|
'<div><span class="status-label">Index</span><strong>' + escapeHtml(indexStatus) + "</strong></div>",
|
|
4356
4376
|
"</div>",
|
|
@@ -4361,6 +4381,50 @@ function renderAppState() {
|
|
|
4361
4381
|
renderExportJobs();
|
|
4362
4382
|
}
|
|
4363
4383
|
|
|
4384
|
+
function renderFolderList() {
|
|
4385
|
+
if (state.folderError) {
|
|
4386
|
+
els.folderList.innerHTML =
|
|
4387
|
+
'<div class="folder-empty folder-empty--error">' + escapeHtml(state.folderError) + "</div>";
|
|
4388
|
+
return;
|
|
4389
|
+
}
|
|
4390
|
+
|
|
4391
|
+
const buttons = [
|
|
4392
|
+
[
|
|
4393
|
+
'<button class="folder-row"' +
|
|
4394
|
+
(state.selectedFolderId ? "" : ' data-selected="true"') +
|
|
4395
|
+
' data-folder-id="">',
|
|
4396
|
+
'<span class="folder-row__title">All meetings</span>',
|
|
4397
|
+
'<span class="folder-row__meta">Browse the full meeting list.</span>',
|
|
4398
|
+
"</button>",
|
|
4399
|
+
].join(""),
|
|
4400
|
+
];
|
|
4401
|
+
|
|
4402
|
+
for (const folder of state.folders) {
|
|
4403
|
+
buttons.push(
|
|
4404
|
+
[
|
|
4405
|
+
'<button class="folder-row"' +
|
|
4406
|
+
(folder.id === state.selectedFolderId ? ' data-selected="true"' : "") +
|
|
4407
|
+
' data-folder-id="' +
|
|
4408
|
+
escapeHtml(folder.id) +
|
|
4409
|
+
'">',
|
|
4410
|
+
'<span class="folder-row__title">' +
|
|
4411
|
+
escapeHtml((folder.isFavourite ? "★ " : "") + (folder.name || folder.id)) +
|
|
4412
|
+
"</span>",
|
|
4413
|
+
'<span class="folder-row__meta">' +
|
|
4414
|
+
escapeHtml(String(folder.documentCount) + " meetings") +
|
|
4415
|
+
"</span>",
|
|
4416
|
+
"</button>",
|
|
4417
|
+
].join(""),
|
|
4418
|
+
);
|
|
4419
|
+
}
|
|
4420
|
+
|
|
4421
|
+
if (buttons.length === 1) {
|
|
4422
|
+
buttons.push('<div class="folder-empty">No folders found.</div>');
|
|
4423
|
+
}
|
|
4424
|
+
|
|
4425
|
+
els.folderList.innerHTML = buttons.join("");
|
|
4426
|
+
}
|
|
4427
|
+
|
|
4364
4428
|
function renderSecurityPanel() {
|
|
4365
4429
|
els.securityPanel.hidden = !state.serverLocked;
|
|
4366
4430
|
}
|
|
@@ -4504,6 +4568,7 @@ function renderMeetingDetail() {
|
|
|
4504
4568
|
"Title: " + (record.meeting.title || record.meeting.id),
|
|
4505
4569
|
"Created: " + record.meeting.createdAt,
|
|
4506
4570
|
"Updated: " + record.meeting.updatedAt,
|
|
4571
|
+
"Folders: " + (record.meeting.folders.length ? record.meeting.folders.map((folder) => folder.name).join(", ") : "none"),
|
|
4507
4572
|
"Tags: " + (record.meeting.tags.length ? record.meeting.tags.join(", ") : "none"),
|
|
4508
4573
|
"Transcript loaded: " + (record.meeting.transcriptLoaded ? "yes" : "no"),
|
|
4509
4574
|
].join("\n");
|
|
@@ -4613,6 +4678,10 @@ function buildMeetingsQuery(limit = 100, refresh = false) {
|
|
|
4613
4678
|
params.set("updatedTo", state.updatedTo);
|
|
4614
4679
|
}
|
|
4615
4680
|
|
|
4681
|
+
if (state.selectedFolderId) {
|
|
4682
|
+
params.set("folderId", state.selectedFolderId);
|
|
4683
|
+
}
|
|
4684
|
+
|
|
4616
4685
|
if (refresh) {
|
|
4617
4686
|
params.set("refresh", "true");
|
|
4618
4687
|
}
|
|
@@ -4620,6 +4689,39 @@ function buildMeetingsQuery(limit = 100, refresh = false) {
|
|
|
4620
4689
|
return "?" + params.toString();
|
|
4621
4690
|
}
|
|
4622
4691
|
|
|
4692
|
+
async function loadFolders(options = {}) {
|
|
4693
|
+
const refresh = options.refresh === true;
|
|
4694
|
+
|
|
4695
|
+
try {
|
|
4696
|
+
state.folderError = "";
|
|
4697
|
+
const params = new URLSearchParams();
|
|
4698
|
+
params.set("limit", "100");
|
|
4699
|
+
if (refresh) {
|
|
4700
|
+
params.set("refresh", "true");
|
|
4701
|
+
}
|
|
4702
|
+
|
|
4703
|
+
const payload = await fetchJson("/folders?" + params.toString());
|
|
4704
|
+
state.folders = payload.folders || [];
|
|
4705
|
+
if (
|
|
4706
|
+
state.selectedFolderId &&
|
|
4707
|
+
!state.folders.some((folder) => folder.id === state.selectedFolderId)
|
|
4708
|
+
) {
|
|
4709
|
+
state.selectedFolderId = null;
|
|
4710
|
+
}
|
|
4711
|
+
} catch (error) {
|
|
4712
|
+
if (error.authRequired) {
|
|
4713
|
+
throw error;
|
|
4714
|
+
}
|
|
4715
|
+
|
|
4716
|
+
state.folderError = error instanceof Error ? error.message : String(error);
|
|
4717
|
+
state.folders = [];
|
|
4718
|
+
state.selectedFolderId = null;
|
|
4719
|
+
}
|
|
4720
|
+
|
|
4721
|
+
renderFolderList();
|
|
4722
|
+
syncBrowserUrl();
|
|
4723
|
+
}
|
|
4724
|
+
|
|
4623
4725
|
async function loadMeetings(options = {}) {
|
|
4624
4726
|
const preferredMeetingId = options.preferredMeetingId || state.selectedMeetingId;
|
|
4625
4727
|
const refresh = options.refresh === true;
|
|
@@ -4683,10 +4785,12 @@ async function quickOpenMeeting() {
|
|
|
4683
4785
|
try {
|
|
4684
4786
|
state.quickOpen = query;
|
|
4685
4787
|
const payload = await fetchJson("/meetings/resolve?q=" + encodeURIComponent(query));
|
|
4788
|
+
state.selectedFolderId = payload.meeting?.meeting?.folders?.[0]?.id || null;
|
|
4686
4789
|
state.search = "";
|
|
4687
4790
|
state.updatedFrom = "";
|
|
4688
4791
|
state.updatedTo = "";
|
|
4689
4792
|
syncFilterInputs();
|
|
4793
|
+
renderFolderList();
|
|
4690
4794
|
await loadMeetings({
|
|
4691
4795
|
preferredMeetingId: payload.document.id,
|
|
4692
4796
|
});
|
|
@@ -4701,11 +4805,9 @@ async function quickOpenMeeting() {
|
|
|
4701
4805
|
async function refreshAll(forceLiveMeetings = false) {
|
|
4702
4806
|
setStatus("Refreshing…", "busy");
|
|
4703
4807
|
try {
|
|
4704
|
-
|
|
4705
|
-
|
|
4706
|
-
|
|
4707
|
-
loadMeetings({ refresh: forceLiveMeetings }),
|
|
4708
|
-
]);
|
|
4808
|
+
await loadFolders({ refresh: forceLiveMeetings });
|
|
4809
|
+
const [appState, authState] = await Promise.all([fetchJson("/state"), fetchJson("/auth/status")]);
|
|
4810
|
+
await loadMeetings({ refresh: forceLiveMeetings });
|
|
4709
4811
|
state.serverLocked = false;
|
|
4710
4812
|
state.appState = {
|
|
4711
4813
|
...appState,
|
|
@@ -4856,17 +4958,40 @@ async function lockServer() {
|
|
|
4856
4958
|
|
|
4857
4959
|
state.serverLocked = true;
|
|
4858
4960
|
state.appState = null;
|
|
4961
|
+
state.folders = [];
|
|
4859
4962
|
state.meetings = [];
|
|
4963
|
+
state.selectedFolderId = null;
|
|
4860
4964
|
state.selectedMeeting = null;
|
|
4861
4965
|
state.selectedMeetingBundle = null;
|
|
4862
4966
|
state.detailError = "";
|
|
4967
|
+
state.folderError = "";
|
|
4863
4968
|
els.serverPassword.value = "";
|
|
4864
4969
|
renderSecurityPanel();
|
|
4970
|
+
renderFolderList();
|
|
4865
4971
|
renderMeetingList();
|
|
4866
4972
|
renderMeetingDetail();
|
|
4867
4973
|
setStatus("Server locked", "error");
|
|
4868
4974
|
}
|
|
4869
4975
|
|
|
4976
|
+
els.folderList.addEventListener("click", (event) => {
|
|
4977
|
+
if (!(event.target instanceof Element)) {
|
|
4978
|
+
return;
|
|
4979
|
+
}
|
|
4980
|
+
|
|
4981
|
+
const button = event.target.closest("[data-folder-id]");
|
|
4982
|
+
if (!button) {
|
|
4983
|
+
return;
|
|
4984
|
+
}
|
|
4985
|
+
|
|
4986
|
+
const nextFolderId = button.dataset.folderId || null;
|
|
4987
|
+
state.selectedFolderId = nextFolderId;
|
|
4988
|
+
state.selectedMeetingId = null;
|
|
4989
|
+
state.selectedMeeting = null;
|
|
4990
|
+
state.selectedMeetingBundle = null;
|
|
4991
|
+
renderFolderList();
|
|
4992
|
+
void loadMeetings();
|
|
4993
|
+
});
|
|
4994
|
+
|
|
4870
4995
|
els.list.addEventListener("click", (event) => {
|
|
4871
4996
|
if (!(event.target instanceof Element)) {
|
|
4872
4997
|
return;
|
|
@@ -5071,6 +5196,7 @@ document.addEventListener("keydown", (event) => {
|
|
|
5071
5196
|
});
|
|
5072
5197
|
|
|
5073
5198
|
const initialSelection = startupSelection();
|
|
5199
|
+
state.selectedFolderId = initialSelection.folderId || null;
|
|
5074
5200
|
state.selectedMeetingId = initialSelection.meetingId || null;
|
|
5075
5201
|
state.workspaceTab = initialSelection.workspaceTab;
|
|
5076
5202
|
|
|
@@ -5086,9 +5212,12 @@ events.addEventListener("state.updated", (event) => {
|
|
|
5086
5212
|
payload.state.documents?.loadedAt &&
|
|
5087
5213
|
payload.state.documents.loadedAt !== previousLoadedAt
|
|
5088
5214
|
) {
|
|
5089
|
-
void
|
|
5090
|
-
|
|
5091
|
-
|
|
5215
|
+
void (async () => {
|
|
5216
|
+
await loadFolders();
|
|
5217
|
+
await loadMeetings({
|
|
5218
|
+
preferredMeetingId: state.selectedMeetingId,
|
|
5219
|
+
});
|
|
5220
|
+
})();
|
|
5092
5221
|
}
|
|
5093
5222
|
});
|
|
5094
5223
|
events.addEventListener("error", () => {
|
|
@@ -5097,6 +5226,7 @@ events.addEventListener("error", () => {
|
|
|
5097
5226
|
|
|
5098
5227
|
syncFilterInputs();
|
|
5099
5228
|
renderSecurityPanel();
|
|
5229
|
+
renderFolderList();
|
|
5100
5230
|
|
|
5101
5231
|
void refreshAll().catch((error) => {
|
|
5102
5232
|
setStatus("Error", "error");
|
|
@@ -5111,7 +5241,7 @@ const granolaWebMarkup = String.raw`
|
|
|
5111
5241
|
<aside class="pane sidebar">
|
|
5112
5242
|
<section class="hero">
|
|
5113
5243
|
<h1>Granola Toolkit</h1>
|
|
5114
|
-
<p>Browser workspace for meetings, notes, transcripts, and export flows on top of one local server instance.</p>
|
|
5244
|
+
<p>Browser workspace for folders, meetings, notes, transcripts, and export flows on top of one local server instance.</p>
|
|
5115
5245
|
<input class="search" data-search placeholder="Search meetings, ids, or tags" />
|
|
5116
5246
|
<div class="field-row field-row--inline">
|
|
5117
5247
|
<label>
|
|
@@ -5133,6 +5263,13 @@ const granolaWebMarkup = String.raw`
|
|
|
5133
5263
|
<input class="field-input" data-updated-to type="date" />
|
|
5134
5264
|
</label>
|
|
5135
5265
|
</section>
|
|
5266
|
+
<section class="folder-panel">
|
|
5267
|
+
<div class="folder-panel__head">
|
|
5268
|
+
<h2>Folders</h2>
|
|
5269
|
+
<p>Pick a folder to scope the meeting browser, or stay on All meetings.</p>
|
|
5270
|
+
</div>
|
|
5271
|
+
<div class="folder-list" data-folder-list></div>
|
|
5272
|
+
</section>
|
|
5136
5273
|
<section class="toolbar">
|
|
5137
5274
|
<div>
|
|
5138
5275
|
<p>Meetings are loaded from the shared server state so this view can later coexist with the terminal UI.</p>
|
|
@@ -5254,11 +5391,11 @@ body {
|
|
|
5254
5391
|
|
|
5255
5392
|
.sidebar {
|
|
5256
5393
|
display: grid;
|
|
5257
|
-
grid-template-rows: auto auto 1fr;
|
|
5394
|
+
grid-template-rows: auto auto auto 1fr;
|
|
5258
5395
|
overflow: hidden;
|
|
5259
5396
|
}
|
|
5260
5397
|
|
|
5261
|
-
.hero, .toolbar, .detail-head {
|
|
5398
|
+
.hero, .toolbar, .detail-head, .folder-panel {
|
|
5262
5399
|
padding: 22px 24px;
|
|
5263
5400
|
border-bottom: 1px solid var(--line);
|
|
5264
5401
|
}
|
|
@@ -5315,6 +5452,68 @@ body {
|
|
|
5315
5452
|
overflow: auto;
|
|
5316
5453
|
}
|
|
5317
5454
|
|
|
5455
|
+
.folder-panel {
|
|
5456
|
+
display: grid;
|
|
5457
|
+
gap: 14px;
|
|
5458
|
+
}
|
|
5459
|
+
|
|
5460
|
+
.folder-panel__head h2 {
|
|
5461
|
+
margin: 0;
|
|
5462
|
+
font-size: 0.92rem;
|
|
5463
|
+
letter-spacing: 0.08em;
|
|
5464
|
+
text-transform: uppercase;
|
|
5465
|
+
}
|
|
5466
|
+
|
|
5467
|
+
.folder-panel__head p {
|
|
5468
|
+
margin: 6px 0 0;
|
|
5469
|
+
color: var(--muted);
|
|
5470
|
+
font-size: 0.9rem;
|
|
5471
|
+
}
|
|
5472
|
+
|
|
5473
|
+
.folder-list {
|
|
5474
|
+
display: grid;
|
|
5475
|
+
gap: 10px;
|
|
5476
|
+
}
|
|
5477
|
+
|
|
5478
|
+
.folder-row {
|
|
5479
|
+
width: 100%;
|
|
5480
|
+
display: grid;
|
|
5481
|
+
gap: 4px;
|
|
5482
|
+
text-align: left;
|
|
5483
|
+
padding: 12px 14px;
|
|
5484
|
+
border: 1px solid transparent;
|
|
5485
|
+
border-radius: 16px;
|
|
5486
|
+
background: rgba(255, 255, 255, 0.72);
|
|
5487
|
+
color: inherit;
|
|
5488
|
+
cursor: pointer;
|
|
5489
|
+
transition: transform 140ms ease, border-color 140ms ease, background 140ms ease;
|
|
5490
|
+
}
|
|
5491
|
+
|
|
5492
|
+
.folder-row:hover,
|
|
5493
|
+
.folder-row[data-selected="true"] {
|
|
5494
|
+
transform: translateY(-1px);
|
|
5495
|
+
border-color: rgba(163, 79, 47, 0.26);
|
|
5496
|
+
background: var(--panel-strong);
|
|
5497
|
+
}
|
|
5498
|
+
|
|
5499
|
+
.folder-row__title {
|
|
5500
|
+
font-weight: 700;
|
|
5501
|
+
}
|
|
5502
|
+
|
|
5503
|
+
.folder-row__meta {
|
|
5504
|
+
color: var(--muted);
|
|
5505
|
+
font-size: 0.88rem;
|
|
5506
|
+
}
|
|
5507
|
+
|
|
5508
|
+
.folder-empty {
|
|
5509
|
+
color: var(--muted);
|
|
5510
|
+
font-size: 0.92rem;
|
|
5511
|
+
}
|
|
5512
|
+
|
|
5513
|
+
.folder-empty--error {
|
|
5514
|
+
color: var(--error);
|
|
5515
|
+
}
|
|
5516
|
+
|
|
5318
5517
|
.meeting-row {
|
|
5319
5518
|
width: 100%;
|
|
5320
5519
|
display: grid;
|
|
@@ -6174,7 +6373,11 @@ function printWebRoutes() {
|
|
|
6174
6373
|
console.log(" GET /auth/status");
|
|
6175
6374
|
console.log(" GET /state");
|
|
6176
6375
|
console.log(" GET /events");
|
|
6376
|
+
console.log(" GET /folders");
|
|
6377
|
+
console.log(" GET /folders/resolve?q=<query>");
|
|
6378
|
+
console.log(" GET /folders/:id");
|
|
6177
6379
|
console.log(" GET /meetings");
|
|
6380
|
+
console.log(" GET /meetings?folderId=<id>");
|
|
6178
6381
|
console.log(" GET /meetings/:id");
|
|
6179
6382
|
console.log(" GET /exports/jobs");
|
|
6180
6383
|
console.log(" POST /auth/login");
|