@unson/brainbase-mcp 0.43.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 +2 -2
- package/ui/meeting-minutes.css +122 -0
- package/ui/meeting-minutes.js +316 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@unson/brainbase-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.44.0",
|
|
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",
|
|
@@ -496,5 +496,5 @@
|
|
|
496
496
|
"engines": {
|
|
497
497
|
"node": ">=20"
|
|
498
498
|
},
|
|
499
|
-
"gitHead": "
|
|
499
|
+
"gitHead": "a695d9dedc1c2bbe6a5ae43c23280c21771387d5"
|
|
500
500
|
}
|
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
|