@unson/brainbase-mcp 0.42.0 → 0.44.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/package.json +4 -2
- package/ui/area-nav.css +56 -4
- package/ui/area-nav.js +130 -10
- package/ui/inbox.css +189 -0
- package/ui/inbox.js +578 -0
- package/ui/local-web-shell.css +4 -2
- package/ui/local-web-shell.js +53 -51
- package/ui/meeting-minutes.css +122 -0
- package/ui/meeting-minutes.js +316 -0
package/ui/local-web-shell.js
CHANGED
|
@@ -61,62 +61,63 @@ const NAV_ICON_PATHS = Object.freeze({
|
|
|
61
61
|
ontology: ['M4 4h6v6H4z', 'M14 14h6v6h-6z', 'M14 4h6v6h-6z', 'M10 7h4', 'M17 10v4'],
|
|
62
62
|
// World view (extension id `world`, ledger P17).
|
|
63
63
|
world: ['M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18z', 'M3 12h18', 'M12 3c2.5 2.6 3.8 5.6 3.8 9s-1.3 6.4-3.8 9c-2.5-2.6-3.8-5.6-3.8-9S9.5 5.6 12 3z'],
|
|
64
|
-
//
|
|
65
|
-
// the organization edition's gear.
|
|
66
|
-
'area:work': ['M4 8h16v11H4z', 'M9 8V5h6v3', 'M4 13h16'],
|
|
64
|
+
// 設定 (ledger D10): the organization edition's gear.
|
|
67
65
|
'area:settings': ['M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915', 'M12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6z'],
|
|
68
66
|
});
|
|
69
67
|
// Screens without their own icon (for example a private product's extension).
|
|
70
68
|
const DEFAULT_NAV_ICON_PATHS = Object.freeze(['M5 20V10m7 10V4m7 16v-7']);
|
|
71
69
|
const SVG_NS = 'http://www.w3.org/2000/svg';
|
|
72
70
|
|
|
73
|
-
// The menu's
|
|
74
|
-
//
|
|
75
|
-
//
|
|
76
|
-
//
|
|
77
|
-
//
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
Object.freeze({ id: '
|
|
81
|
-
Object.freeze({ id: '
|
|
82
|
-
Object.freeze({ id: '
|
|
83
|
-
Object.freeze({ id: 'settings', label: '
|
|
71
|
+
// The menu's sections (brainbase-project ledger D10, 2026-10-17 revision): the
|
|
72
|
+
// destinations themselves under small headings, in the organization edition's
|
|
73
|
+
// order, with 設定 at the bottom. Screens OSS Personal does not have are not
|
|
74
|
+
// listed. `screenId` makes the heading open that screen (プロジェクトと関係者);
|
|
75
|
+
// `area` turns the section's screens into one line with tabs (設定). Inside a
|
|
76
|
+
// section, `order` lists the screens first in that order.
|
|
77
|
+
const NAV_SECTIONS = Object.freeze([
|
|
78
|
+
Object.freeze({ id: 'meetings', label: '会議', order: ['minutes'] }),
|
|
79
|
+
Object.freeze({ id: 'project', label: 'プロジェクト', screenId: 'projects', order: ['objectives'] }),
|
|
80
|
+
Object.freeze({ id: 'explore', label: '調べる', order: ['value-proofs', 'graph', 'people', 'memory'] }),
|
|
81
|
+
Object.freeze({ id: 'settings', label: null, placement: 'footer', order: ['ontology'], area: Object.freeze({ label: '設定', defaultScreen: 'ontology', icon: 'area:settings' }) }),
|
|
84
82
|
]);
|
|
85
83
|
|
|
84
|
+
const sectionOf = (screen) => NAV_SECTIONS.find((section) => section.id === screen.section) ?? null;
|
|
85
|
+
|
|
86
86
|
// Menu order only: the screens' own order (and so the first screen) stays as given.
|
|
87
|
-
// Screens without
|
|
88
|
-
// screen come first (a leading extension such as the world), the rest come
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
const
|
|
93
|
-
const
|
|
94
|
-
const
|
|
95
|
-
|
|
87
|
+
// Screens without a section keep their place — those before the first sectioned
|
|
88
|
+
// screen come first (a leading extension such as the world), the rest come after
|
|
89
|
+
// the sections and before 設定.
|
|
90
|
+
function navAreas(screens) {
|
|
91
|
+
const own = (screen, section = null) => ({ id: screen.id, label: null, screenIds: [screen.id], icon: screen.id, ...(section ? { section } : {}) });
|
|
92
|
+
const firstSectioned = screens.findIndex((screen) => sectionOf(screen));
|
|
93
|
+
const leading = firstSectioned === -1 ? [...screens] : screens.slice(0, firstSectioned);
|
|
94
|
+
const trailing = firstSectioned === -1 ? [] : screens.slice(firstSectioned).filter((screen) => !sectionOf(screen));
|
|
95
|
+
const sectioned = NAV_SECTIONS.map((section) => {
|
|
96
|
+
const members = screens.filter((screen) => screen.section === section.id && screen.id !== section.screenId);
|
|
96
97
|
const rank = (screen) => {
|
|
97
|
-
const index =
|
|
98
|
-
return index === -1 ?
|
|
98
|
+
const index = section.order.indexOf(screen.id);
|
|
99
|
+
return index === -1 ? section.order.length + members.indexOf(screen) : index;
|
|
99
100
|
};
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
101
|
+
const ordered = [...members].sort((a, b) => rank(a) - rank(b));
|
|
102
|
+
if (ordered.length === 0) return { section, areas: [] };
|
|
103
|
+
if (section.area) {
|
|
104
|
+
return { section, areas: [{ id: section.id, label: section.area.label, defaultScreen: section.area.defaultScreen, screenIds: ordered.map((screen) => screen.id), icon: section.area.icon, section: section.id }] };
|
|
105
|
+
}
|
|
106
|
+
return { section, areas: ordered.map((screen) => own(screen, section.id)) };
|
|
107
|
+
});
|
|
108
|
+
const main = sectioned.filter(({ section }) => section.placement !== 'footer').flatMap(({ areas }) => areas);
|
|
109
|
+
const footer = sectioned.filter(({ section }) => section.placement === 'footer').flatMap(({ areas }) => areas);
|
|
110
|
+
return [...leading.map((screen) => own(screen)), ...main, ...trailing.map((screen) => own(screen)), ...footer];
|
|
103
111
|
}
|
|
104
112
|
|
|
105
|
-
//
|
|
106
|
-
function
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
...
|
|
112
|
-
|
|
113
|
-
label: area.label,
|
|
114
|
-
defaultScreen: area.defaultScreen,
|
|
115
|
-
screenIds: members.map((screen) => screen.id),
|
|
116
|
-
icon: area.icon,
|
|
117
|
-
})),
|
|
118
|
-
...layout.trailing.map(own),
|
|
119
|
-
];
|
|
113
|
+
// The sections handed to the menu part; a heading opens its screen only when that screen exists.
|
|
114
|
+
function navSections(screens) {
|
|
115
|
+
return NAV_SECTIONS.map((section) => ({
|
|
116
|
+
id: section.id,
|
|
117
|
+
label: section.label,
|
|
118
|
+
placement: section.placement ?? 'main',
|
|
119
|
+
...(section.screenId && screens.some((screen) => screen.id === section.screenId) ? { screenId: section.screenId } : {}),
|
|
120
|
+
}));
|
|
120
121
|
}
|
|
121
122
|
|
|
122
123
|
function makeIcon(doc, id) {
|
|
@@ -376,19 +377,19 @@ function mountGraphScreen(screenId, createView, extra = {}) {
|
|
|
376
377
|
}
|
|
377
378
|
|
|
378
379
|
export const LOCAL_WEB_SCREENS = Object.freeze([
|
|
379
|
-
Object.freeze({ id: 'value-proofs', label: '判断の履歴',
|
|
380
|
-
Object.freeze({ id: 'minutes', label: '議事録',
|
|
381
|
-
Object.freeze({ id: 'objectives', label: '目的と現状',
|
|
382
|
-
Object.freeze({ id: 'projects', label: 'プロジェクトと関係者',
|
|
380
|
+
Object.freeze({ id: 'value-proofs', label: '判断の履歴', section: 'explore', usesGraph: false, rail: true, source: '判断の記録', mount: mountToday }),
|
|
381
|
+
Object.freeze({ id: 'minutes', label: '議事録', section: 'meetings', usesGraph: false, rail: true, source: '正本の議事録', mount: mountMeetingMinutes }),
|
|
382
|
+
Object.freeze({ id: 'objectives', label: '目的と現状', section: 'project', usesGraph: true, rail: true, source: '手元のGraph', mount: mountObjectives }),
|
|
383
|
+
Object.freeze({ id: 'projects', label: 'プロジェクトと関係者', section: 'project', usesGraph: true, rail: true, source: '手元のGraph', mount: mountGraphScreen('projects', createGraphProjectsView) }),
|
|
383
384
|
// A record of 情報と関係 opens as its own screen at `#graph?entity_id=…`; the list is `#graph`.
|
|
384
|
-
Object.freeze({ id: 'graph', label: '情報と関係',
|
|
385
|
+
Object.freeze({ id: 'graph', label: '情報と関係', section: 'explore', usesGraph: true, rail: false, source: '手元のGraph', mount: mountGraphScreen('graph', createGraphRegistryView, (context) => ({
|
|
385
386
|
onRouteChange: (entityId, options) => context.navigate?.(entityId ? `graph?entity_id=${encodeURIComponent(entityId)}` : 'graph', options),
|
|
386
387
|
})) }),
|
|
387
|
-
Object.freeze({ id: 'people', label: '人と会社',
|
|
388
|
+
Object.freeze({ id: 'people', label: '人と会社', section: 'explore', usesGraph: true, rail: true, source: '手元のGraph', mount: mountGraphScreen('people', createPeopleAndCompaniesView, { canCorrect: true }) }),
|
|
388
389
|
// Only the owner sees it (ledger D15); the local host has one owner, so it is always listed here.
|
|
389
|
-
Object.freeze({ id: 'memory', label: 'あなたの記憶',
|
|
390
|
+
Object.freeze({ id: 'memory', label: 'あなたの記憶', section: 'explore', usesGraph: false, rail: true, ownerOnly: true, source: '手元の個人KG', mount: mountPersonalMemory }),
|
|
390
391
|
// The definitions behind 情報と関係: infrequent management work, so under settings (ledger D14).
|
|
391
|
-
Object.freeze({ id: 'ontology', label: '情報の種類とつながり方',
|
|
392
|
+
Object.freeze({ id: 'ontology', label: '情報の種類とつながり方', section: 'settings', usesGraph: true, rail: false, source: '手元のGraph', mount: mountGraphScreen('ontology', createGraphOntologyView, { canCorrect: true }) }),
|
|
392
393
|
]);
|
|
393
394
|
|
|
394
395
|
function renderGraphGate(doc, slot, status, onRecheck, label) {
|
|
@@ -516,6 +517,7 @@ export function createLocalWebShell({
|
|
|
516
517
|
const areaNav = createAreaNav({
|
|
517
518
|
document: doc,
|
|
518
519
|
areas: areaList,
|
|
520
|
+
sections: navSections(screens),
|
|
519
521
|
screens: screens.map((screen) => ({ id: screen.id, label: screen.label })),
|
|
520
522
|
memory: areaMemory,
|
|
521
523
|
linkClassName: 'bb-shell-nav-link',
|
package/ui/meeting-minutes.css
CHANGED
|
@@ -698,3 +698,125 @@
|
|
|
698
698
|
.bb-minutes-connection-note.is-unavailable {
|
|
699
699
|
color: var(--bb-color-warning);
|
|
700
700
|
}
|
|
701
|
+
|
|
702
|
+
/* 参加者 (story-meeting-participants-row-v1): linked names, 未確認 badges and the link panel. */
|
|
703
|
+
.bb-minutes-participants {
|
|
704
|
+
display: grid;
|
|
705
|
+
gap: 4px;
|
|
706
|
+
margin: 8px 0 4px;
|
|
707
|
+
padding: 8px 12px;
|
|
708
|
+
border: 1px solid var(--bb-color-border);
|
|
709
|
+
border-radius: var(--bb-radius-md, 6px);
|
|
710
|
+
background: var(--bb-color-surface);
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
.bb-minutes-participants[hidden] {
|
|
714
|
+
display: none;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
.bb-minutes-participants-row {
|
|
718
|
+
display: grid;
|
|
719
|
+
grid-template-columns: 7em minmax(0, 1fr);
|
|
720
|
+
gap: 8px;
|
|
721
|
+
align-items: baseline;
|
|
722
|
+
font-size: 13px;
|
|
723
|
+
line-height: 1.5;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
.bb-minutes-participant-items {
|
|
727
|
+
display: flex;
|
|
728
|
+
flex-wrap: wrap;
|
|
729
|
+
gap: 4px 14px;
|
|
730
|
+
margin: 0;
|
|
731
|
+
padding: 0;
|
|
732
|
+
list-style: none;
|
|
733
|
+
min-width: 0;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
.bb-minutes-participant-items li {
|
|
737
|
+
display: inline-flex;
|
|
738
|
+
align-items: baseline;
|
|
739
|
+
gap: 4px;
|
|
740
|
+
min-width: 0;
|
|
741
|
+
overflow-wrap: anywhere;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
.bb-minutes-participant-badge {
|
|
745
|
+
padding: 0 8px;
|
|
746
|
+
border: 1px dashed var(--bb-color-warning);
|
|
747
|
+
border-radius: 999px;
|
|
748
|
+
background: transparent;
|
|
749
|
+
color: var(--bb-color-ink);
|
|
750
|
+
font: inherit;
|
|
751
|
+
cursor: pointer;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
.bb-minutes-participant-badge[aria-expanded="true"] {
|
|
755
|
+
border-style: solid;
|
|
756
|
+
background: var(--bb-color-warning-soft);
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
.bb-minutes-participant-undo {
|
|
760
|
+
padding: 0;
|
|
761
|
+
border: 0;
|
|
762
|
+
background: transparent;
|
|
763
|
+
color: var(--bb-color-muted);
|
|
764
|
+
font-size: 12px;
|
|
765
|
+
text-decoration: underline;
|
|
766
|
+
cursor: pointer;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
.bb-minutes-participant-notice {
|
|
770
|
+
margin: 0;
|
|
771
|
+
font-size: 13px;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
.bb-minutes-participant-panel {
|
|
775
|
+
display: grid;
|
|
776
|
+
gap: 8px;
|
|
777
|
+
margin-top: 4px;
|
|
778
|
+
padding: 10px 12px;
|
|
779
|
+
border: 1px solid var(--bb-color-border);
|
|
780
|
+
border-radius: var(--bb-radius-md, 6px);
|
|
781
|
+
background: var(--bb-color-paper);
|
|
782
|
+
font-size: 13px;
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
.bb-minutes-participant-panel-title {
|
|
786
|
+
margin: 0;
|
|
787
|
+
font-weight: 700;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
.bb-minutes-participant-others {
|
|
791
|
+
margin: 0;
|
|
792
|
+
color: var(--bb-color-muted);
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
.bb-minutes-participant-candidates {
|
|
796
|
+
display: grid;
|
|
797
|
+
gap: 6px;
|
|
798
|
+
margin: 0;
|
|
799
|
+
padding: 0;
|
|
800
|
+
list-style: none;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
.bb-minutes-participant-candidate {
|
|
804
|
+
display: flex;
|
|
805
|
+
flex-wrap: wrap;
|
|
806
|
+
align-items: center;
|
|
807
|
+
justify-content: space-between;
|
|
808
|
+
gap: 4px 12px;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
.bb-minutes-participant-candidate > div {
|
|
812
|
+
display: grid;
|
|
813
|
+
min-width: 0;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
.bb-minutes-participant-new,
|
|
817
|
+
.bb-minutes-participant-not-person {
|
|
818
|
+
display: flex;
|
|
819
|
+
flex-wrap: wrap;
|
|
820
|
+
align-items: flex-end;
|
|
821
|
+
gap: 6px 8px;
|
|
822
|
+
}
|
package/ui/meeting-minutes.js
CHANGED
|
@@ -208,6 +208,52 @@ function normalizeConnections(value) {
|
|
|
208
208
|
]));
|
|
209
209
|
}
|
|
210
210
|
|
|
211
|
+
// 参加者の行 (story-meeting-participants-row-v1). The host decides who is linked,
|
|
212
|
+
// unconfirmed or not a person; anything malformed is unreadable, never a person (P9).
|
|
213
|
+
const PARTICIPANT_STATES = new Set(['linked', 'unconfirmed', 'not_person']);
|
|
214
|
+
|
|
215
|
+
function normalizeParticipant(value) {
|
|
216
|
+
if (!isRecord(value) || !PARTICIPANT_STATES.has(value.state)) return null;
|
|
217
|
+
const displayName = text(value.display_name);
|
|
218
|
+
if (!displayName) return null;
|
|
219
|
+
const others = Number(value.other_meetings);
|
|
220
|
+
const participant = {
|
|
221
|
+
displayName,
|
|
222
|
+
state: value.state,
|
|
223
|
+
removable: value.removable === true,
|
|
224
|
+
otherMeetings: Number.isInteger(others) && others > 0 ? others : 0,
|
|
225
|
+
};
|
|
226
|
+
if (value.state === 'linked') {
|
|
227
|
+
const name = text(value.name);
|
|
228
|
+
const entityId = text(value.entity_id);
|
|
229
|
+
if (!name || !entityId) return null;
|
|
230
|
+
participant.name = name;
|
|
231
|
+
participant.entityId = entityId;
|
|
232
|
+
participant.href = connectionHref(value.href);
|
|
233
|
+
}
|
|
234
|
+
return participant;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function normalizeParticipants(value) {
|
|
238
|
+
if (!isRecord(value) || !['recorded', 'none', 'unavailable'].includes(value.status)) return { status: 'unavailable' };
|
|
239
|
+
if (value.status !== 'recorded') return { status: value.status };
|
|
240
|
+
const participants = Array.isArray(value.participants) ? value.participants.map(normalizeParticipant) : [];
|
|
241
|
+
if (participants.some((participant) => participant === null)) return { status: 'unavailable' };
|
|
242
|
+
if (!participants.length) return { status: 'none' };
|
|
243
|
+
return { status: 'recorded', participants, redrafting: value.redrafting === true };
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function normalizeCandidateList(values, idField) {
|
|
247
|
+
if (!Array.isArray(values)) return [];
|
|
248
|
+
return values.flatMap((value) => {
|
|
249
|
+
const id = text(value?.[idField]);
|
|
250
|
+
const name = text(value?.name);
|
|
251
|
+
if (!id || !name) return [];
|
|
252
|
+
const evidence = Array.isArray(value.evidence) ? value.evidence.map((item) => text(item)).filter(Boolean) : [];
|
|
253
|
+
return [{ id, name, company: text(value.company), evidence }];
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
|
|
211
257
|
function firstDate(values) {
|
|
212
258
|
for (const value of values) {
|
|
213
259
|
const date = parseDate(value);
|
|
@@ -636,6 +682,7 @@ export function createMeetingMinutesUI({
|
|
|
636
682
|
versionActionExtensions = [],
|
|
637
683
|
meetingLabel,
|
|
638
684
|
meetingConnections,
|
|
685
|
+
meetingParticipants,
|
|
639
686
|
canCreateMeeting,
|
|
640
687
|
createUnavailableReason,
|
|
641
688
|
canEditMinutes,
|
|
@@ -666,11 +713,14 @@ export function createMeetingMinutesUI({
|
|
|
666
713
|
withdrawingMinutesId: null,
|
|
667
714
|
// { meetingId, rows } where rows is null while the host is answering.
|
|
668
715
|
connections: null,
|
|
716
|
+
// { meetingId, value, openName } for the 参加者 row (story-meeting-participants-row-v1).
|
|
717
|
+
participants: null,
|
|
669
718
|
};
|
|
670
719
|
let listRequestGeneration = 0;
|
|
671
720
|
let detailRequestGeneration = 0;
|
|
672
721
|
let createReasonSequence = 0;
|
|
673
722
|
let connectionsSlot = null;
|
|
723
|
+
let participantsSlot = null;
|
|
674
724
|
let railProjectSlot = null;
|
|
675
725
|
let railProjectMeeting = null;
|
|
676
726
|
|
|
@@ -1052,6 +1102,260 @@ export function createMeetingMinutesUI({
|
|
|
1052
1102
|
if (railProjectSlot && railProjectMeeting?.meeting_id === meetingId) fillRailProject(railProjectSlot, railProjectMeeting);
|
|
1053
1103
|
}
|
|
1054
1104
|
|
|
1105
|
+
function hasParticipants() {
|
|
1106
|
+
return isRecord(meetingParticipants) && typeof meetingParticipants.read === 'function';
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
function currentParticipants(meeting) {
|
|
1110
|
+
const value = state.participants;
|
|
1111
|
+
return value && value.meetingId === meeting?.meeting_id ? value : null;
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
function participantError(error, fallback) {
|
|
1115
|
+
const reason = text(error?.message);
|
|
1116
|
+
return reason && !/^[a-z_]+$/u.test(reason) ? reason : fallback;
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
function fillParticipants(slot, meeting) {
|
|
1120
|
+
const current = currentParticipants(meeting);
|
|
1121
|
+
const value = current?.value ?? null;
|
|
1122
|
+
slot.hidden = value?.status === 'none';
|
|
1123
|
+
if (value?.status === 'none') {
|
|
1124
|
+
slot.replaceChildren();
|
|
1125
|
+
return;
|
|
1126
|
+
}
|
|
1127
|
+
const row = makeElement(doc, 'div', { className: 'bb-minutes-participants-row' });
|
|
1128
|
+
row.append(makeElement(doc, 'span', { className: 'bb-minutes-connection-label', textContent: '参加者' }));
|
|
1129
|
+
if (!value) {
|
|
1130
|
+
row.append(makeElement(doc, 'p', { className: 'bb-minutes-connection-note', textContent: '読み込んでいます' }));
|
|
1131
|
+
slot.replaceChildren(row);
|
|
1132
|
+
return;
|
|
1133
|
+
}
|
|
1134
|
+
if (value.status !== 'recorded') {
|
|
1135
|
+
row.append(makeElement(doc, 'p', { className: 'bb-minutes-connection-note is-unavailable', textContent: '参加者を読み込めませんでした' }));
|
|
1136
|
+
slot.replaceChildren(row);
|
|
1137
|
+
return;
|
|
1138
|
+
}
|
|
1139
|
+
const list = makeElement(doc, 'ul', { className: 'bb-minutes-participant-items' });
|
|
1140
|
+
for (const participant of value.participants) {
|
|
1141
|
+
const entry = makeElement(doc, 'li', { attrs: { 'data-participant-state': participant.state } });
|
|
1142
|
+
if (participant.state === 'linked') {
|
|
1143
|
+
entry.append(connectionLink({ label: participant.name, href: participant.href }, 'bb-minutes-connection-link'));
|
|
1144
|
+
if (participant.name !== participant.displayName) {
|
|
1145
|
+
entry.append(makeElement(doc, 'span', { className: 'bb-minutes-connection-detail', textContent: `(${participant.displayName})` }));
|
|
1146
|
+
}
|
|
1147
|
+
} else if (participant.state === 'unconfirmed') {
|
|
1148
|
+
const open = current.openName === participant.displayName;
|
|
1149
|
+
const badge = button(doc, `${participant.displayName}(未確認)`, () => toggleParticipantPanel(meeting, participant.displayName), {
|
|
1150
|
+
className: 'bb-minutes-participant-badge',
|
|
1151
|
+
attrs: { 'aria-expanded': open ? 'true' : 'false' },
|
|
1152
|
+
});
|
|
1153
|
+
entry.append(badge);
|
|
1154
|
+
} else {
|
|
1155
|
+
entry.append(makeElement(doc, 'span', { textContent: participant.displayName }));
|
|
1156
|
+
entry.append(makeElement(doc, 'span', { className: 'bb-minutes-connection-detail', textContent: '(人ではない)' }));
|
|
1157
|
+
}
|
|
1158
|
+
if (participant.removable && typeof meetingParticipants.unlink === 'function') {
|
|
1159
|
+
entry.append(button(doc, '取り消す', () => void unlinkParticipant(meeting, participant), { className: 'bb-minutes-participant-undo' }));
|
|
1160
|
+
}
|
|
1161
|
+
list.append(entry);
|
|
1162
|
+
}
|
|
1163
|
+
row.append(list);
|
|
1164
|
+
const children = [row];
|
|
1165
|
+
if (value.redrafting) {
|
|
1166
|
+
children.push(makeElement(doc, 'p', { className: 'bb-minutes-connection-note', textContent: '名前を結んだので、この議事録を作り直しています。' }));
|
|
1167
|
+
}
|
|
1168
|
+
if (current.notice) {
|
|
1169
|
+
children.push(makeElement(doc, 'p', { className: 'bb-minutes-participant-notice', textContent: current.notice, attrs: { role: 'status' } }));
|
|
1170
|
+
}
|
|
1171
|
+
if (current.openName && value.participants.some((participant) => participant.state === 'unconfirmed' && participant.displayName === current.openName)) {
|
|
1172
|
+
children.push(renderParticipantPanel(meeting, current));
|
|
1173
|
+
}
|
|
1174
|
+
slot.replaceChildren(...children);
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
function refreshParticipants(meeting) {
|
|
1178
|
+
if (participantsSlot && state.detail?.meeting?.meeting_id === meeting.meeting_id) fillParticipants(participantsSlot, meeting);
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
async function loadParticipants(detail, isCurrentRequest, { notice = '' } = {}) {
|
|
1182
|
+
const meetingId = detail.meeting.meeting_id;
|
|
1183
|
+
const previous = currentParticipants(detail.meeting);
|
|
1184
|
+
state.participants = { meetingId, value: previous?.value ?? null, openName: null, notice };
|
|
1185
|
+
let value;
|
|
1186
|
+
try {
|
|
1187
|
+
value = normalizeParticipants(await meetingParticipants.read({
|
|
1188
|
+
meeting: detail.meeting,
|
|
1189
|
+
minutes: detail.minutes,
|
|
1190
|
+
versions: detail.versions,
|
|
1191
|
+
}));
|
|
1192
|
+
} catch {
|
|
1193
|
+
value = { status: 'unavailable' };
|
|
1194
|
+
}
|
|
1195
|
+
if (!isCurrentRequest() || state.participants?.meetingId !== meetingId) return;
|
|
1196
|
+
state.participants = { ...state.participants, value };
|
|
1197
|
+
refreshParticipants(detail.meeting);
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
function reloadParticipants(meeting, notice) {
|
|
1201
|
+
const detail = state.detail;
|
|
1202
|
+
if (!detail || detail.meeting?.meeting_id !== meeting.meeting_id) return Promise.resolve();
|
|
1203
|
+
return loadParticipants(detail, () => state.detail?.meeting?.meeting_id === meeting.meeting_id, { notice });
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
function toggleParticipantPanel(meeting, displayName) {
|
|
1207
|
+
const current = currentParticipants(meeting);
|
|
1208
|
+
if (!current) return;
|
|
1209
|
+
const closing = current.openName === displayName;
|
|
1210
|
+
state.participants = { ...current, openName: closing ? null : displayName, panel: closing ? null : { status: 'loading' }, notice: '' };
|
|
1211
|
+
refreshParticipants(meeting);
|
|
1212
|
+
if (!closing) void loadCandidates(meeting, displayName);
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
async function loadCandidates(meeting, displayName) {
|
|
1216
|
+
let panel;
|
|
1217
|
+
try {
|
|
1218
|
+
if (typeof meetingParticipants.candidates !== 'function') throw new Error('候補を読み込めませんでした');
|
|
1219
|
+
const value = await meetingParticipants.candidates({ meeting }, displayName);
|
|
1220
|
+
if (!isRecord(value)) throw new Error('候補を読み込めませんでした');
|
|
1221
|
+
const others = Number(value.other_meetings);
|
|
1222
|
+
panel = {
|
|
1223
|
+
status: 'ready',
|
|
1224
|
+
people: normalizeCandidateList(value.people, 'entity_id'),
|
|
1225
|
+
contacts: normalizeCandidateList(value.contacts, 'contact_id'),
|
|
1226
|
+
companies: normalizeCandidateList(value.companies, 'entity_id'),
|
|
1227
|
+
otherMeetings: Number.isInteger(others) && others > 0 ? others : 0,
|
|
1228
|
+
};
|
|
1229
|
+
} catch (error) {
|
|
1230
|
+
panel = { status: 'error', message: participantError(error, '候補を読み込めませんでした') };
|
|
1231
|
+
}
|
|
1232
|
+
const current = currentParticipants(meeting);
|
|
1233
|
+
if (!current || current.openName !== displayName) return;
|
|
1234
|
+
state.participants = { ...current, panel: { ...panel, name: current.panel?.name ?? displayName } };
|
|
1235
|
+
refreshParticipants(meeting);
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
function candidateRow(name, evidence, actionLabel, onClick, busy) {
|
|
1239
|
+
const item = makeElement(doc, 'li', { className: 'bb-minutes-participant-candidate' });
|
|
1240
|
+
const body = makeElement(doc, 'div');
|
|
1241
|
+
body.append(makeElement(doc, 'strong', { textContent: name }));
|
|
1242
|
+
if (evidence.length) body.append(makeElement(doc, 'span', { className: 'bb-minutes-connection-detail', textContent: evidence.join('・') }));
|
|
1243
|
+
item.append(body, button(doc, actionLabel, onClick, { disabled: busy }));
|
|
1244
|
+
return item;
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
function renderParticipantPanel(meeting, current) {
|
|
1248
|
+
const displayName = current.openName;
|
|
1249
|
+
const panelState = current.panel ?? { status: 'loading' };
|
|
1250
|
+
const busy = panelState.busy === true;
|
|
1251
|
+
const panel = makeElement(doc, 'div', {
|
|
1252
|
+
className: 'bb-minutes-participant-panel',
|
|
1253
|
+
attrs: { role: 'group', 'aria-label': `${displayName}を人物に結ぶ` },
|
|
1254
|
+
});
|
|
1255
|
+
panel.append(makeElement(doc, 'p', { className: 'bb-minutes-participant-panel-title', textContent: `「${displayName}」は誰ですか` }));
|
|
1256
|
+
if (panelState.status === 'loading') {
|
|
1257
|
+
panel.append(makeElement(doc, 'p', { className: 'bb-minutes-connection-note', textContent: '候補を読み込んでいます' }));
|
|
1258
|
+
} else if (panelState.status === 'error' && !panelState.people) {
|
|
1259
|
+
panel.append(makeElement(doc, 'p', { className: 'bb-minutes-connection-note is-unavailable', textContent: panelState.message, attrs: { role: 'alert' } }));
|
|
1260
|
+
}
|
|
1261
|
+
if (panelState.people) {
|
|
1262
|
+
if (panelState.otherMeetings > 0) {
|
|
1263
|
+
panel.append(makeElement(doc, 'p', {
|
|
1264
|
+
className: 'bb-minutes-participant-others',
|
|
1265
|
+
textContent: `同じ表示名の議事録があと${panelState.otherMeetings}件あります。結ぶとそれらも作り直します。`,
|
|
1266
|
+
}));
|
|
1267
|
+
}
|
|
1268
|
+
const list = makeElement(doc, 'ul', { className: 'bb-minutes-participant-candidates' });
|
|
1269
|
+
for (const person of panelState.people) {
|
|
1270
|
+
list.append(candidateRow(person.name, person.evidence, 'この人', () => void linkParticipant(meeting, displayName, { action: 'person', entity_id: person.id }), busy));
|
|
1271
|
+
}
|
|
1272
|
+
for (const contact of panelState.contacts) {
|
|
1273
|
+
const evidence = contact.evidence.length ? contact.evidence : contact.company ? [`名刺:${contact.company}`] : ['名刺'];
|
|
1274
|
+
list.append(candidateRow(contact.name, evidence, 'この名刺の名前で登録', () => void linkParticipant(meeting, displayName, { action: 'new_person', name: contact.name, contact_id: contact.id }), busy));
|
|
1275
|
+
}
|
|
1276
|
+
if (!panelState.people.length && !panelState.contacts.length) {
|
|
1277
|
+
panel.append(makeElement(doc, 'p', { className: 'bb-minutes-connection-note', textContent: '人物と名刺に、それらしい候補はありませんでした。' }));
|
|
1278
|
+
} else {
|
|
1279
|
+
panel.append(list);
|
|
1280
|
+
}
|
|
1281
|
+
const form = makeElement(doc, 'div', { className: 'bb-minutes-participant-new' });
|
|
1282
|
+
const field = labelInput(doc, '新しい人として登録する名前', { value: panelState.name ?? displayName, name: 'participant_name' });
|
|
1283
|
+
field.input.addEventListener('input', () => {
|
|
1284
|
+
const latest = currentParticipants(meeting);
|
|
1285
|
+
if (latest?.panel) latest.panel.name = field.input.value;
|
|
1286
|
+
});
|
|
1287
|
+
form.append(field.wrapper, button(doc, '新しい人として登録', () => void linkParticipant(meeting, displayName, {
|
|
1288
|
+
action: 'new_person',
|
|
1289
|
+
name: text(field.input.value, displayName),
|
|
1290
|
+
}), { disabled: busy, className: 'bb-minutes-button is-primary' }));
|
|
1291
|
+
panel.append(form);
|
|
1292
|
+
const notPerson = makeElement(doc, 'div', { className: 'bb-minutes-participant-not-person' });
|
|
1293
|
+
notPerson.append(makeElement(doc, 'span', { className: 'bb-minutes-connection-label', textContent: '人ではない' }));
|
|
1294
|
+
for (const company of panelState.companies) {
|
|
1295
|
+
notPerson.append(button(doc, `${company.name}(会社)に結ぶ`, () => void linkParticipant(meeting, displayName, { action: 'company', entity_id: company.id }), { disabled: busy }));
|
|
1296
|
+
}
|
|
1297
|
+
notPerson.append(button(doc, '結ばない', () => void linkParticipant(meeting, displayName, { action: 'not_person' }), { disabled: busy }));
|
|
1298
|
+
panel.append(notPerson);
|
|
1299
|
+
if (panelState.message) {
|
|
1300
|
+
panel.append(makeElement(doc, 'p', { className: 'bb-minutes-connection-note is-unavailable', textContent: panelState.message, attrs: { role: 'alert' } }));
|
|
1301
|
+
}
|
|
1302
|
+
if (panelState.existing) {
|
|
1303
|
+
panel.append(candidateRow(panelState.existing.name, ['同じ名前の人がいます'], 'この人', () => void linkParticipant(meeting, displayName, { action: 'person', entity_id: panelState.existing.id }), busy));
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
panel.append(button(doc, 'あとで', () => toggleParticipantPanel(meeting, displayName), { className: 'bb-minutes-button is-quiet' }));
|
|
1307
|
+
return panel;
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
function setPanel(meeting, displayName, patch) {
|
|
1311
|
+
const current = currentParticipants(meeting);
|
|
1312
|
+
if (!current || current.openName !== displayName) return false;
|
|
1313
|
+
state.participants = { ...current, panel: { ...current.panel, ...patch } };
|
|
1314
|
+
refreshParticipants(meeting);
|
|
1315
|
+
return true;
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
function redraftNotice(result, done) {
|
|
1319
|
+
const count = Number(result?.redraft?.meetings);
|
|
1320
|
+
return Number.isInteger(count) && count > 0
|
|
1321
|
+
? `${done}議事録${count}件を作り直します(数分かかります)。`
|
|
1322
|
+
: `${done}`;
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
async function linkParticipant(meeting, displayName, input) {
|
|
1326
|
+
if (typeof meetingParticipants.link !== 'function') return;
|
|
1327
|
+
if (!setPanel(meeting, displayName, { busy: true, message: '', existing: null })) return;
|
|
1328
|
+
try {
|
|
1329
|
+
const result = await meetingParticipants.link({ meeting }, { display_name: displayName, ...input });
|
|
1330
|
+
const linkedName = text(result?.linked?.name);
|
|
1331
|
+
await reloadParticipants(meeting, redraftNotice(result, linkedName ? `「${displayName}」を${linkedName}に結びました。` : `「${displayName}」を記録しました。`));
|
|
1332
|
+
} catch (error) {
|
|
1333
|
+
const existing = isRecord(error?.candidate) && text(error.candidate.entity_id) && text(error.candidate.name)
|
|
1334
|
+
? { id: text(error.candidate.entity_id), name: text(error.candidate.name) }
|
|
1335
|
+
: null;
|
|
1336
|
+
setPanel(meeting, displayName, {
|
|
1337
|
+
busy: false,
|
|
1338
|
+
existing,
|
|
1339
|
+
message: existing ? '同じ名前の人がいます。この人で良ければ選んでください。' : participantError(error, '結べませんでした。もう一度試してください。'),
|
|
1340
|
+
});
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
async function unlinkParticipant(meeting, participant) {
|
|
1345
|
+
try {
|
|
1346
|
+
const result = await meetingParticipants.unlink({ meeting }, {
|
|
1347
|
+
display_name: participant.displayName,
|
|
1348
|
+
...(participant.entityId ? { entity_id: participant.entityId } : {}),
|
|
1349
|
+
});
|
|
1350
|
+
await reloadParticipants(meeting, redraftNotice(result, `「${participant.displayName}」の結び付きを取り消しました。`));
|
|
1351
|
+
} catch (error) {
|
|
1352
|
+
const current = currentParticipants(meeting);
|
|
1353
|
+
if (!current) return;
|
|
1354
|
+
state.participants = { ...current, notice: participantError(error, '取り消せませんでした。') };
|
|
1355
|
+
refreshParticipants(meeting);
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1055
1359
|
function appendLoadError(parent, message, retry) {
|
|
1056
1360
|
const notice = makeElement(doc, 'div', {
|
|
1057
1361
|
className: 'bb-minutes-load-error',
|
|
@@ -1448,6 +1752,7 @@ export function createMeetingMinutesUI({
|
|
|
1448
1752
|
function renderDetail(detail, documentRecord = null, selectedVersion = null) {
|
|
1449
1753
|
clear(detailPane);
|
|
1450
1754
|
connectionsSlot = null;
|
|
1755
|
+
participantsSlot = null;
|
|
1451
1756
|
if (state.detailError) {
|
|
1452
1757
|
const retry = state.meetingId === null ? loadMeetings : () => loadDetail(state.meetingId);
|
|
1453
1758
|
appendLoadError(detailPane, state.detailError.startsWith('会議詳細を読み込めませんでした。')
|
|
@@ -1480,6 +1785,15 @@ export function createMeetingMinutesUI({
|
|
|
1480
1785
|
textContent: participants ? `${when} / ${participants}` : when,
|
|
1481
1786
|
}));
|
|
1482
1787
|
detailPane.append(header);
|
|
1788
|
+
participantsSlot = null;
|
|
1789
|
+
if (hasParticipants()) {
|
|
1790
|
+
participantsSlot = makeElement(doc, 'section', {
|
|
1791
|
+
className: 'bb-minutes-participants',
|
|
1792
|
+
attrs: { 'aria-label': '参加者' },
|
|
1793
|
+
});
|
|
1794
|
+
fillParticipants(participantsSlot, meeting);
|
|
1795
|
+
detailPane.append(participantsSlot);
|
|
1796
|
+
}
|
|
1483
1797
|
if (hasConnections()) {
|
|
1484
1798
|
connectionsSlot = makeElement(doc, 'section', {
|
|
1485
1799
|
className: 'bb-minutes-connections',
|
|
@@ -1631,9 +1945,11 @@ export function createMeetingMinutesUI({
|
|
|
1631
1945
|
state.detail = detail;
|
|
1632
1946
|
state.detailError = null;
|
|
1633
1947
|
if (hasConnections()) state.connections = { meetingId, rows: null };
|
|
1948
|
+
if (hasParticipants() && state.participants?.meetingId !== meetingId) state.participants = null;
|
|
1634
1949
|
renderDetail(detail);
|
|
1635
1950
|
setStatus('会議詳細を読み込みました。');
|
|
1636
1951
|
if (hasConnections()) void loadConnections(detail, isCurrentRequest);
|
|
1952
|
+
if (hasParticipants()) void loadParticipants(detail, isCurrentRequest);
|
|
1637
1953
|
} catch (error) {
|
|
1638
1954
|
if (!isCurrentRequest()) return state.detail;
|
|
1639
1955
|
const preserved = preserveLoadedStateOnFailure(error) && state.detail?.meeting?.meeting_id === meetingId
|