@unson/brainbase-mcp 0.41.0 → 0.41.1
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/package.json +2 -2
- package/ui/meeting-minutes.css +19 -0
- package/ui/meeting-minutes.js +117 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@unson/brainbase-mcp",
|
|
3
|
-
"version": "0.41.
|
|
3
|
+
"version": "0.41.1",
|
|
4
4
|
"description": "Local-first Brainbase judgment OS that lets supported AI agents reuse a user's approved goals, priorities, decisions, and judgment criteria.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -481,5 +481,5 @@
|
|
|
481
481
|
"engines": {
|
|
482
482
|
"node": ">=20"
|
|
483
483
|
},
|
|
484
|
-
"gitHead": "
|
|
484
|
+
"gitHead": "d89de84f6d437c564f843ee06dcdf5836a154d36"
|
|
485
485
|
}
|
package/ui/meeting-minutes.css
CHANGED
|
@@ -16,12 +16,31 @@
|
|
|
16
16
|
.bb-minutes-eyebrow,
|
|
17
17
|
.bb-minutes-label,
|
|
18
18
|
.bb-minutes-item-meta,
|
|
19
|
+
.bb-minutes-meeting-label,
|
|
19
20
|
.bb-minutes-rail-title,
|
|
20
21
|
.bb-minutes-revision {
|
|
21
22
|
color: var(--bb-color-muted);
|
|
22
23
|
font-size: 0.82rem;
|
|
23
24
|
}
|
|
24
25
|
|
|
26
|
+
.bb-minutes-meeting-label {
|
|
27
|
+
margin: 0;
|
|
28
|
+
overflow-wrap: anywhere;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.bb-minutes-create-guard {
|
|
32
|
+
flex-basis: 100%;
|
|
33
|
+
margin: 0;
|
|
34
|
+
color: var(--bb-color-danger, var(--bb-color-ink));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.bb-minutes-edit-guard {
|
|
38
|
+
display: grid;
|
|
39
|
+
gap: var(--bb-space-2);
|
|
40
|
+
margin: 0;
|
|
41
|
+
color: var(--bb-color-danger, var(--bb-color-ink));
|
|
42
|
+
}
|
|
43
|
+
|
|
25
44
|
.bb-minutes-heading h1,
|
|
26
45
|
.bb-minutes-heading p,
|
|
27
46
|
.bb-minutes-pane-head h2,
|
package/ui/meeting-minutes.js
CHANGED
|
@@ -16,6 +16,15 @@ function text(value, fallback = '') {
|
|
|
16
16
|
return typeof value === 'string' && value.trim() ? value.trim() : fallback;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
function meetingLabelText(meetingLabel, meeting) {
|
|
20
|
+
if (typeof meetingLabel !== 'function') return '';
|
|
21
|
+
try {
|
|
22
|
+
return text(meetingLabel(meeting));
|
|
23
|
+
} catch {
|
|
24
|
+
return '';
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
19
28
|
function makeElement(doc, tag, { className, textContent, type, value, disabled, attrs = {} } = {}) {
|
|
20
29
|
const element = doc.createElement(tag);
|
|
21
30
|
if (className) element.className = className;
|
|
@@ -205,6 +214,9 @@ export function createMeetingMinutesUI({
|
|
|
205
214
|
token,
|
|
206
215
|
basePath = '/api/meeting-minutes',
|
|
207
216
|
versionActionExtensions = [],
|
|
217
|
+
meetingLabel,
|
|
218
|
+
canCreateMeeting,
|
|
219
|
+
canEditMinutes,
|
|
208
220
|
} = {}) {
|
|
209
221
|
if (!root) throw new TypeError('root is required');
|
|
210
222
|
const doc = explicitDocument ?? (typeof document === 'undefined' ? null : document);
|
|
@@ -213,6 +225,8 @@ export function createMeetingMinutesUI({
|
|
|
213
225
|
? fetcher
|
|
214
226
|
: typeof globalThis.fetch === 'function' ? (path, init) => globalThis.fetch(path, init) : null;
|
|
215
227
|
const base = String(basePath).replace(/\/+$/u, '');
|
|
228
|
+
const createBlockedMessage = 'この画面では会議を作成できません。';
|
|
229
|
+
const editBlockedMessage = 'この画面では議事録を編集できません。';
|
|
216
230
|
const state = {
|
|
217
231
|
meetings: [],
|
|
218
232
|
detail: null,
|
|
@@ -223,6 +237,8 @@ export function createMeetingMinutesUI({
|
|
|
223
237
|
listError: null,
|
|
224
238
|
detailError: null,
|
|
225
239
|
};
|
|
240
|
+
let listRequestGeneration = 0;
|
|
241
|
+
let detailRequestGeneration = 0;
|
|
226
242
|
|
|
227
243
|
const screen = makeElement(doc, 'div', { className: 'bb-minutes-screen', attrs: { 'data-contract-version': MEETING_MINUTES_UI_CONTRACT_VERSION } });
|
|
228
244
|
const heading = makeElement(doc, 'header', { className: 'bb-minutes-heading' });
|
|
@@ -274,6 +290,49 @@ export function createMeetingMinutesUI({
|
|
|
274
290
|
|
|
275
291
|
function clear(node) { node.replaceChildren(); return node; }
|
|
276
292
|
|
|
293
|
+
function canCreate() {
|
|
294
|
+
if (typeof canCreateMeeting === 'function') {
|
|
295
|
+
try {
|
|
296
|
+
return canCreateMeeting() === true;
|
|
297
|
+
} catch {
|
|
298
|
+
return false;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
return canCreateMeeting !== false;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function canEdit() {
|
|
305
|
+
if (typeof canEditMinutes === 'function') {
|
|
306
|
+
try {
|
|
307
|
+
return canEditMinutes() === true;
|
|
308
|
+
} catch {
|
|
309
|
+
return false;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
return canEditMinutes !== false;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function appendCreateBlockedNotice(parent, { alert = false } = {}) {
|
|
316
|
+
parent.append(makeElement(doc, 'p', {
|
|
317
|
+
className: 'bb-minutes-create-guard',
|
|
318
|
+
textContent: createBlockedMessage,
|
|
319
|
+
attrs: alert ? { role: 'alert' } : {},
|
|
320
|
+
}));
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function appendEditBlockedNotice(parent, { alert = false } = {}) {
|
|
324
|
+
const existing = [...(parent.children ?? [])].find((child) => child?.className === 'bb-minutes-edit-guard');
|
|
325
|
+
if (existing) {
|
|
326
|
+
if (alert) existing.setAttribute('role', 'alert');
|
|
327
|
+
return existing;
|
|
328
|
+
}
|
|
329
|
+
parent.append(makeElement(doc, 'p', {
|
|
330
|
+
className: 'bb-minutes-edit-guard',
|
|
331
|
+
textContent: editBlockedMessage,
|
|
332
|
+
attrs: alert ? { role: 'alert' } : {},
|
|
333
|
+
}));
|
|
334
|
+
}
|
|
335
|
+
|
|
277
336
|
function appendLoadError(parent, message, retry) {
|
|
278
337
|
const notice = makeElement(doc, 'div', {
|
|
279
338
|
className: 'bb-minutes-load-error',
|
|
@@ -288,7 +347,12 @@ export function createMeetingMinutesUI({
|
|
|
288
347
|
clear(listPane);
|
|
289
348
|
const head = makeElement(doc, 'div', { className: 'bb-minutes-pane-head' });
|
|
290
349
|
head.append(makeElement(doc, 'h2', { textContent: '会議一覧' }));
|
|
291
|
-
|
|
350
|
+
const createAllowed = canCreate();
|
|
351
|
+
head.append(button(doc, '会議を作成', () => renderCreateForm(), {
|
|
352
|
+
className: 'bb-minutes-button is-primary',
|
|
353
|
+
disabled: !createAllowed,
|
|
354
|
+
}));
|
|
355
|
+
if (!createAllowed) appendCreateBlockedNotice(head);
|
|
292
356
|
listPane.append(head);
|
|
293
357
|
if (state.listError) {
|
|
294
358
|
appendLoadError(listPane, `会議一覧を読み込めませんでした。理由: ${state.listError}`, () => loadMeetings({ restoreFocus: true }));
|
|
@@ -307,8 +371,10 @@ export function createMeetingMinutesUI({
|
|
|
307
371
|
type: 'button',
|
|
308
372
|
attrs: { type: 'button' },
|
|
309
373
|
});
|
|
374
|
+
const label = meetingLabelText(meetingLabel, meeting);
|
|
310
375
|
item.append(
|
|
311
376
|
makeElement(doc, 'strong', { textContent: text(meeting.title, '無題の会議') }),
|
|
377
|
+
...(label ? [makeElement(doc, 'span', { className: 'bb-minutes-meeting-label', textContent: label })] : []),
|
|
312
378
|
makeElement(doc, 'span', { className: 'bb-minutes-item-meta', textContent: displayDate(meeting.scheduled_at) }),
|
|
313
379
|
makeElement(doc, 'span', { className: 'bb-minutes-item-meta', textContent: `${Array.isArray(entry.minutes) ? entry.minutes.length : 0}件の議事録` }),
|
|
314
380
|
);
|
|
@@ -382,8 +448,14 @@ export function createMeetingMinutesUI({
|
|
|
382
448
|
section.append(content);
|
|
383
449
|
if (selected) {
|
|
384
450
|
const actions = makeElement(doc, 'div', { className: 'bb-minutes-actions' });
|
|
451
|
+
const editAllowed = canEdit();
|
|
385
452
|
const confirm = button(doc, selected.confirmation ? '確認済み' : 'この版を確認', async () => {
|
|
386
453
|
if (selected.confirmation) return;
|
|
454
|
+
if (!canEdit()) {
|
|
455
|
+
confirm.disabled = true;
|
|
456
|
+
appendEditBlockedNotice(section, { alert: true });
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
387
459
|
confirm.disabled = true;
|
|
388
460
|
try {
|
|
389
461
|
await mutate(`/${encodeURIComponent(detail.meeting.meeting_id)}/minutes/${encodeURIComponent(documentRecord.minutes_id)}/confirm`, {
|
|
@@ -396,9 +468,10 @@ export function createMeetingMinutesUI({
|
|
|
396
468
|
confirm.disabled = false;
|
|
397
469
|
setStatus(error instanceof Error ? error.message : '確認を保存できませんでした。', 'error');
|
|
398
470
|
}
|
|
399
|
-
}, { className: 'bb-minutes-button is-primary', disabled: Boolean(selected.confirmation) });
|
|
471
|
+
}, { className: 'bb-minutes-button is-primary', disabled: Boolean(selected.confirmation) || !editAllowed });
|
|
400
472
|
actions.append(confirm);
|
|
401
473
|
section.append(actions);
|
|
474
|
+
if (!editAllowed) appendEditBlockedNotice(section);
|
|
402
475
|
}
|
|
403
476
|
appendVersionActionExtensions(doc, section, {
|
|
404
477
|
extensions: versionActionExtensions,
|
|
@@ -422,6 +495,10 @@ export function createMeetingMinutesUI({
|
|
|
422
495
|
}
|
|
423
496
|
|
|
424
497
|
function appendSaveForm(section, detail, documentRecord) {
|
|
498
|
+
if (!canEdit()) {
|
|
499
|
+
appendEditBlockedNotice(section);
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
425
502
|
const form = makeElement(doc, 'form', { className: 'bb-minutes-save-form' });
|
|
426
503
|
form.append(makeElement(doc, 'h4', { textContent: '訂正版を保存' }));
|
|
427
504
|
const field = labelInput(doc, '本文(保存すると新しい版になります)', { type: 'textarea', name: 'body', placeholder: '議事録の本文を入力' });
|
|
@@ -430,6 +507,11 @@ export function createMeetingMinutesUI({
|
|
|
430
507
|
form.append(field.wrapper, save);
|
|
431
508
|
form.addEventListener('submit', async (event) => {
|
|
432
509
|
event.preventDefault();
|
|
510
|
+
if (!canEdit()) {
|
|
511
|
+
save.disabled = false;
|
|
512
|
+
appendEditBlockedNotice(section, { alert: true });
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
433
515
|
save.disabled = true;
|
|
434
516
|
try {
|
|
435
517
|
await mutate(`/${encodeURIComponent(detail.meeting.meeting_id)}/minutes/${encodeURIComponent(documentRecord.minutes_id)}`, {
|
|
@@ -466,6 +548,8 @@ export function createMeetingMinutesUI({
|
|
|
466
548
|
const meeting = detail.meeting;
|
|
467
549
|
const header = makeElement(doc, 'header', { className: 'bb-minutes-detail-head' });
|
|
468
550
|
header.append(makeElement(doc, 'h2', { textContent: text(meeting.title, '無題の会議') }));
|
|
551
|
+
const label = meetingLabelText(meetingLabel, meeting);
|
|
552
|
+
if (label) header.append(makeElement(doc, 'p', { className: 'bb-minutes-meeting-label', textContent: label }));
|
|
469
553
|
header.append(makeElement(doc, 'p', { className: 'bb-minutes-item-meta', textContent: `${displayDate(meeting.scheduled_at)} / ${Array.isArray(meeting.participant_ids) && meeting.participant_ids.length ? meeting.participant_ids.join('、') : '参加者未登録'}` }));
|
|
470
554
|
detailPane.append(header);
|
|
471
555
|
const documents = Array.isArray(detail.minutes) ? detail.minutes : [];
|
|
@@ -477,6 +561,11 @@ export function createMeetingMinutesUI({
|
|
|
477
561
|
}
|
|
478
562
|
|
|
479
563
|
function renderCreateMinutesForm(detail) {
|
|
564
|
+
if (!canEdit()) {
|
|
565
|
+
const guard = makeElement(doc, 'section', { className: 'bb-minutes-edit-guard' });
|
|
566
|
+
appendEditBlockedNotice(guard);
|
|
567
|
+
return guard;
|
|
568
|
+
}
|
|
480
569
|
const form = makeElement(doc, 'form', { className: 'bb-minutes-create-form' });
|
|
481
570
|
form.append(makeElement(doc, 'h3', { textContent: '議事録文書を追加' }));
|
|
482
571
|
const title = labelInput(doc, '議事録の名前', { name: 'title', value: '議事録' });
|
|
@@ -486,6 +575,11 @@ export function createMeetingMinutesUI({
|
|
|
486
575
|
form.append(title.wrapper, body.wrapper, save);
|
|
487
576
|
form.addEventListener('submit', async (event) => {
|
|
488
577
|
event.preventDefault();
|
|
578
|
+
if (!canEdit()) {
|
|
579
|
+
save.disabled = false;
|
|
580
|
+
appendEditBlockedNotice(form, { alert: true });
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
489
583
|
save.disabled = true;
|
|
490
584
|
try {
|
|
491
585
|
await mutate(`/${encodeURIComponent(detail.meeting.meeting_id)}/minutes`, {
|
|
@@ -506,6 +600,10 @@ export function createMeetingMinutesUI({
|
|
|
506
600
|
|
|
507
601
|
function renderCreateForm() {
|
|
508
602
|
clear(detailPane);
|
|
603
|
+
if (!canCreate()) {
|
|
604
|
+
appendCreateBlockedNotice(detailPane, { alert: true });
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
509
607
|
const form = makeElement(doc, 'form', { className: 'bb-minutes-create-form' });
|
|
510
608
|
form.append(makeElement(doc, 'h2', { textContent: '会議を作成' }), makeElement(doc, 'p', { className: 'bb-minutes-item-meta', textContent: '外部サービスを接続しなくても、会議と議事録を保存できます。' }));
|
|
511
609
|
const title = labelInput(doc, '会議名', { name: 'title', placeholder: '例: 週次定例' });
|
|
@@ -518,6 +616,11 @@ export function createMeetingMinutesUI({
|
|
|
518
616
|
form.append(title.wrapper, date.wrapper, participants.wrapper, minutesTitle.wrapper, body.wrapper, save);
|
|
519
617
|
form.addEventListener('submit', async (event) => {
|
|
520
618
|
event.preventDefault();
|
|
619
|
+
if (!canCreate()) {
|
|
620
|
+
save.disabled = false;
|
|
621
|
+
appendCreateBlockedNotice(detailPane, { alert: true });
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
521
624
|
save.disabled = true;
|
|
522
625
|
try {
|
|
523
626
|
const scheduledAt = date.input.value ? new Date(date.input.value).toISOString() : undefined;
|
|
@@ -542,16 +645,20 @@ export function createMeetingMinutesUI({
|
|
|
542
645
|
}
|
|
543
646
|
|
|
544
647
|
async function loadMeetings({ restoreFocus = false } = {}) {
|
|
648
|
+
const requestGeneration = ++listRequestGeneration;
|
|
649
|
+
const isCurrentRequest = () => requestGeneration === listRequestGeneration;
|
|
545
650
|
state.loading = true;
|
|
546
651
|
setStatus('会議一覧を読み込んでいます。');
|
|
547
652
|
try {
|
|
548
653
|
const payload = await call('');
|
|
654
|
+
if (!isCurrentRequest()) return state.meetings;
|
|
549
655
|
if (!isRecord(payload) || !Array.isArray(payload.meetings) || payload.absence_confirmed !== true) throw new Error('会議一覧の形式を確認できません。');
|
|
550
656
|
state.meetings = payload.meetings;
|
|
551
657
|
state.listError = null;
|
|
552
658
|
const selectedMeetingStillListed = state.meetingId !== null
|
|
553
659
|
&& state.meetings.some((entry) => entry?.meeting?.meeting_id === state.meetingId);
|
|
554
660
|
if (state.meetingId !== null && !selectedMeetingStillListed) {
|
|
661
|
+
detailRequestGeneration += 1;
|
|
555
662
|
state.detail = null;
|
|
556
663
|
state.meetingId = null;
|
|
557
664
|
state.minutesId = null;
|
|
@@ -567,12 +674,14 @@ export function createMeetingMinutesUI({
|
|
|
567
674
|
setStatus(state.meetings.length ? `${state.meetings.length}件の会議を読み込みました。` : '保存された会議はありません。');
|
|
568
675
|
if (restoreFocus && typeof listPane.focus === 'function') listPane.focus({ preventScroll: true });
|
|
569
676
|
} catch (error) {
|
|
677
|
+
if (!isCurrentRequest()) return state.meetings;
|
|
570
678
|
state.listError = loadErrorMessage(error, '会議一覧を読み込めませんでした。');
|
|
571
679
|
const preserve = preserveLoadedStateOnFailure(error);
|
|
572
680
|
const hadDetail = Boolean(state.detail) || state.meetingId !== null;
|
|
573
681
|
if (!preserve) {
|
|
574
682
|
state.meetings = [];
|
|
575
683
|
if (hadDetail) {
|
|
684
|
+
detailRequestGeneration += 1;
|
|
576
685
|
state.detail = null;
|
|
577
686
|
state.meetingId = null;
|
|
578
687
|
state.minutesId = null;
|
|
@@ -584,12 +693,15 @@ export function createMeetingMinutesUI({
|
|
|
584
693
|
if (!preserve && hadDetail) renderDetail(null);
|
|
585
694
|
setStatus(state.listError, 'error');
|
|
586
695
|
} finally {
|
|
587
|
-
state.loading = false;
|
|
696
|
+
if (isCurrentRequest()) state.loading = false;
|
|
588
697
|
}
|
|
589
698
|
return state.meetings;
|
|
590
699
|
}
|
|
591
700
|
|
|
592
701
|
async function loadDetail(meetingId) {
|
|
702
|
+
const requestGeneration = ++detailRequestGeneration;
|
|
703
|
+
const isCurrentRequest = () => requestGeneration === detailRequestGeneration
|
|
704
|
+
&& state.meetingId === meetingId;
|
|
593
705
|
state.meetingId = meetingId;
|
|
594
706
|
state.minutesId = null;
|
|
595
707
|
state.detailError = null;
|
|
@@ -597,12 +709,14 @@ export function createMeetingMinutesUI({
|
|
|
597
709
|
setStatus('会議詳細を読み込んでいます。');
|
|
598
710
|
try {
|
|
599
711
|
const detail = await call(`/${encodeURIComponent(meetingId)}`);
|
|
712
|
+
if (!isCurrentRequest()) return state.detail;
|
|
600
713
|
if (!isRecord(detail) || !isRecord(detail.meeting) || !Array.isArray(detail.minutes) || !Array.isArray(detail.versions)) throw new Error('会議詳細の形式を確認できません。');
|
|
601
714
|
state.detail = detail;
|
|
602
715
|
state.detailError = null;
|
|
603
716
|
renderDetail(detail);
|
|
604
717
|
setStatus('会議詳細を読み込みました。');
|
|
605
718
|
} catch (error) {
|
|
719
|
+
if (!isCurrentRequest()) return state.detail;
|
|
606
720
|
const preserved = preserveLoadedStateOnFailure(error) && state.detail?.meeting?.meeting_id === meetingId
|
|
607
721
|
? state.detail
|
|
608
722
|
: null;
|