@takagaki/cortex-decisions-viewer 0.12.9 → 0.12.10
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/dist/build.js +78 -0
- package/dist/render.js +252 -13
- package/package.json +1 -1
package/dist/build.js
CHANGED
|
@@ -39,6 +39,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
39
39
|
exports.readFleetSources = readFleetSources;
|
|
40
40
|
exports.readGoldProposals = readGoldProposals;
|
|
41
41
|
exports.buildHomeGrounding = buildHomeGrounding;
|
|
42
|
+
exports.collectMeetings = collectMeetings;
|
|
42
43
|
exports.findBacklogDir = findBacklogDir;
|
|
43
44
|
exports.build = build;
|
|
44
45
|
exports.createRefResolver = createRefResolver;
|
|
@@ -797,6 +798,79 @@ async function findDirByMarker(repoRoot, marker, fallback) {
|
|
|
797
798
|
}
|
|
798
799
|
return fallback;
|
|
799
800
|
}
|
|
801
|
+
/** 会議の回を入れるディレクトリの名前(`会議/<会議名>/20260120/`) */
|
|
802
|
+
const MEETING_DATE_DIR = /^\d{8}$/;
|
|
803
|
+
/**
|
|
804
|
+
* すでにある会議を列挙する(#277。投入モーダルの「会議名」の選択肢になる)。
|
|
805
|
+
*
|
|
806
|
+
* **「会議名」は画面から自由に打てるが、打たせるのは最後の手段にする。** 手で打つと
|
|
807
|
+
* 表記ゆれ(`定例` と `定例MTG`)で同じ会議が2つのディレクトリに割れ、議事録の連番も
|
|
808
|
+
* 履歴も分かれる。すでにある名前を選べれば、その事故はほとんど起きない。
|
|
809
|
+
*
|
|
810
|
+
* 会議とみなすのは「**日付ディレクトリ(`YYYYMMDD`)を直接ぶら下げているディレクトリ**」。
|
|
811
|
+
* 名前の規則ではなく実際の中身で決めるので、`会議/定例/デイリースクラム/20260120/` のような
|
|
812
|
+
* 入れ子でも当たる(相対パスは `定例/デイリースクラム`)。
|
|
813
|
+
*
|
|
814
|
+
* **会議ディレクトリ直下に日付ディレクトリがある形(相対パスが空)は除外する。**
|
|
815
|
+
* その形は「会議名なしで投入された回」で、選択肢に出しても送り先にできない
|
|
816
|
+
* (`meeting` が空文字になり Lambda が 400 を返す)。
|
|
817
|
+
*
|
|
818
|
+
* `minuteDates` は、その日付ディレクトリに議事録(`*_minutes.md`)が**すでにある**日付。
|
|
819
|
+
* 同じ日に文字起こしを足しても議事録は作り直されないので、送る前に画面で伝えるために使う。
|
|
820
|
+
*/
|
|
821
|
+
async function collectMeetings(repoRoot) {
|
|
822
|
+
const meetingDir = await findDirByMarker(repoRoot, "ingest-config.json", "会議");
|
|
823
|
+
const rootAbs = path.join(repoRoot, meetingDir);
|
|
824
|
+
const out = [];
|
|
825
|
+
const hasMinute = async (dirAbs) => {
|
|
826
|
+
try {
|
|
827
|
+
for (const e of await node_fs_1.promises.readdir(dirAbs, { withFileTypes: true })) {
|
|
828
|
+
if (e.isFile() && e.name.endsWith("_minutes.md"))
|
|
829
|
+
return true;
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
catch {
|
|
833
|
+
// 読めない回は「議事録なし」扱い(注意を出さないだけで、投入は妨げない)
|
|
834
|
+
}
|
|
835
|
+
return false;
|
|
836
|
+
};
|
|
837
|
+
const walk = async (relParts) => {
|
|
838
|
+
let entries;
|
|
839
|
+
try {
|
|
840
|
+
entries = await node_fs_1.promises.readdir(path.join(rootAbs, ...relParts), { withFileTypes: true });
|
|
841
|
+
}
|
|
842
|
+
catch {
|
|
843
|
+
return; // 会議ディレクトリが無い案件(未導入)はここで終わる
|
|
844
|
+
}
|
|
845
|
+
const dirs = entries.filter((e) => e.isDirectory() &&
|
|
846
|
+
e.name !== "node_modules" &&
|
|
847
|
+
e.name !== "templates" &&
|
|
848
|
+
!e.name.startsWith("."));
|
|
849
|
+
const rel = relParts.join("/");
|
|
850
|
+
const dates = dirs.filter((d) => MEETING_DATE_DIR.test(d.name)).map((d) => d.name);
|
|
851
|
+
if (dates.length && rel) {
|
|
852
|
+
const minuteDates = [];
|
|
853
|
+
for (const d of dates.sort()) {
|
|
854
|
+
if (await hasMinute(path.join(rootAbs, ...relParts, d)))
|
|
855
|
+
minuteDates.push(d);
|
|
856
|
+
}
|
|
857
|
+
out.push({ path: rel, minuteDates });
|
|
858
|
+
}
|
|
859
|
+
// 日付ディレクトリの中は回の中身なので降りない(会議名にはならない)。
|
|
860
|
+
// 深さは Lambda 側の上限(isValidMeetingDir の 5 セグメント)と揃える——
|
|
861
|
+
// それより深い会議を選択肢に出しても、選んだ瞬間に 400 で断られるだけ
|
|
862
|
+
if (relParts.length >= 5)
|
|
863
|
+
return;
|
|
864
|
+
for (const d of dirs) {
|
|
865
|
+
if (MEETING_DATE_DIR.test(d.name))
|
|
866
|
+
continue;
|
|
867
|
+
await walk([...relParts, d.name]);
|
|
868
|
+
}
|
|
869
|
+
};
|
|
870
|
+
await walk([]);
|
|
871
|
+
out.sort((a, b) => a.path.localeCompare(b.path, "ja"));
|
|
872
|
+
return out;
|
|
873
|
+
}
|
|
800
874
|
/** exporter が作る区画名(engine の `BACKLOG_SECTIONS` と同じ固定の名前) */
|
|
801
875
|
const BACKLOG_SECTIONS = ["issues", "documents", "wiki"];
|
|
802
876
|
/**
|
|
@@ -1543,6 +1617,9 @@ async function build(opts) {
|
|
|
1543
1617
|
computeLayout(graph); // 各ノードに x/y を書き込む(クライアントはこれを初期位置に使いウォームアップ省略)
|
|
1544
1618
|
// データソースと自動化(fleet-status.json 由来)。無ければ undefined でセクション非表示。
|
|
1545
1619
|
const fleet = await readFleetSources(repoRoot);
|
|
1620
|
+
// すでにある会議(投入モーダルの「会議名」の選択肢)。intake の有無に関わらず載せる——
|
|
1621
|
+
// 出し分けは表示側の仕事で、ここで絞ると「投入の口を後から付けたら選択肢が空」になる
|
|
1622
|
+
const meetings = await collectMeetings(repoRoot);
|
|
1546
1623
|
const site = {
|
|
1547
1624
|
title: opts.title,
|
|
1548
1625
|
generatedAt: opts.generatedAt,
|
|
@@ -1564,6 +1641,7 @@ async function build(opts) {
|
|
|
1564
1641
|
projectKey: opts.projectKey,
|
|
1565
1642
|
}
|
|
1566
1643
|
: undefined,
|
|
1644
|
+
meetings,
|
|
1567
1645
|
// **URLとトークンが両方そろったときだけ出す。** 片方だけでは貼れるURLを作れないので、
|
|
1568
1646
|
// 手順だけが出て「専用URLが空」という一番混乱する形になる(intake と同じ扱い)
|
|
1569
1647
|
backlogWebhook: opts.webhookUrl && opts.webhookToken
|
package/dist/render.js
CHANGED
|
@@ -2740,6 +2740,21 @@ button.ops-card-btn:disabled { opacity: .6; cursor: default; }
|
|
|
2740
2740
|
.file-chip > span:first-child { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 260px; }
|
|
2741
2741
|
.file-chip-x { border: none; background: none; cursor: pointer; color: var(--muted); font-size: 16px; line-height: 1; padding: 0 4px; }
|
|
2742
2742
|
.file-chip-x:hover { color: #dc2626; }
|
|
2743
|
+
/* 会議日(#277)。ファイル名から推せなかったチップは黄色にして「当日を仮置きした」ことを見せる */
|
|
2744
|
+
.file-chip-date {
|
|
2745
|
+
border: 1px solid var(--border); border-radius: 6px; padding: 1px 5px; font-size: 11.5px;
|
|
2746
|
+
color: var(--text); background: var(--surface); font-family: inherit;
|
|
2747
|
+
}
|
|
2748
|
+
.file-chip.is-unguessed { background: #fffbeb; border-color: #fcd34d; color: #92400e; }
|
|
2749
|
+
.file-chip-guess { font-size: 11px; font-weight: 700; color: #92400e; white-space: nowrap; }
|
|
2750
|
+
/* 会議名の欄(選択肢+新規入力+入力エラー) */
|
|
2751
|
+
.meeting-field { display: flex; flex-direction: column; gap: 6px; }
|
|
2752
|
+
.meeting-error { min-height: 0; }
|
|
2753
|
+
/* 「その日はもう議事録がある」の注意。送信は妨げないので、エラー(赤)とは色を変える */
|
|
2754
|
+
.modal-warn {
|
|
2755
|
+
background: #fffbeb; border: 1px solid #fcd34d; border-radius: 8px;
|
|
2756
|
+
padding: 8px 12px; font-size: 12.5px; color: #92400e; line-height: 1.7;
|
|
2757
|
+
}
|
|
2743
2758
|
.summary-box { background: var(--accent-weak); border: 1px solid #dce6fb; border-radius: var(--radius); padding: 12px 16px; color: #1e3a8a; }
|
|
2744
2759
|
.deciders { display: flex; flex-wrap: wrap; gap: 8px; }
|
|
2745
2760
|
.refs { margin: 0; padding-left: 18px; }
|
|
@@ -9673,6 +9688,46 @@ ${deriveCapTile.toString()}
|
|
|
9673
9688
|
]);
|
|
9674
9689
|
var nameInput = el("input", { type: "text", class: "modal-name", placeholder: "投入者として記録されます" });
|
|
9675
9690
|
modal.appendChild(fieldRow("ファイル(必須)", dropzone));
|
|
9691
|
+
|
|
9692
|
+
// ---- 会議名と会議日(文字起こしのみ・#277)----
|
|
9693
|
+
//
|
|
9694
|
+
// **どの会議の・いつの回か**が決まらないと、送ったファイルは置き場所を持てない。
|
|
9695
|
+
// 以前は名前も日付もサーバ側の推測に任せていたので、同じ会議が表記ゆれで2つの
|
|
9696
|
+
// ディレクトリに割れたり、別の日の回として入ったりした。ここで人に確定させる。
|
|
9697
|
+
var meetingList = isT && SITE.meetings ? SITE.meetings : [];
|
|
9698
|
+
var MEETING_NEW = "__new__";
|
|
9699
|
+
var BACKSLASH = String.fromCharCode(92);
|
|
9700
|
+
var meetingSelect = null;
|
|
9701
|
+
var meetingNewInput = null;
|
|
9702
|
+
var meetingError = null;
|
|
9703
|
+
var dupWarn = null;
|
|
9704
|
+
// ユーザーが会議を自分で選んだか。**選んだ後はファイル名からの推測で上書きしない**
|
|
9705
|
+
var meetingTouched = false;
|
|
9706
|
+
if (isT) {
|
|
9707
|
+
var meetingBox = el("div", { class: "meeting-field" });
|
|
9708
|
+
if (meetingList.length) {
|
|
9709
|
+
meetingSelect = el("select", { class: "modal-name meeting-select" });
|
|
9710
|
+
meetingSelect.appendChild(el("option", { value: "", text: "会議を選択してください" }));
|
|
9711
|
+
meetingList.forEach(function (m) {
|
|
9712
|
+
meetingSelect.appendChild(el("option", { value: m.path, text: m.path }));
|
|
9713
|
+
});
|
|
9714
|
+
meetingSelect.appendChild(el("option", { value: MEETING_NEW, text: "+ 新しい会議名を入力" }));
|
|
9715
|
+
meetingBox.appendChild(meetingSelect);
|
|
9716
|
+
}
|
|
9717
|
+
meetingNewInput = el("input", { type: "text", class: "modal-name meeting-new", placeholder: "新しい会議名(例: デイリースクラム)" });
|
|
9718
|
+
// 選択肢がある案件では、明示的に「新しい会議名」を選ぶまで隠す。
|
|
9719
|
+
// 選択肢が1件も無い案件(初回投入)は入力欄だけを出す
|
|
9720
|
+
if (meetingSelect) meetingNewInput.hidden = true;
|
|
9721
|
+
meetingBox.appendChild(meetingNewInput);
|
|
9722
|
+
meetingError = el("div", { class: "modal-status modal-status-error meeting-error" });
|
|
9723
|
+
meetingError.hidden = true;
|
|
9724
|
+
meetingBox.appendChild(meetingError);
|
|
9725
|
+
modal.appendChild(fieldRow("会議名(必須)", meetingBox));
|
|
9726
|
+
dupWarn = el("div", { class: "modal-warn meeting-dup-warn" });
|
|
9727
|
+
dupWarn.hidden = true;
|
|
9728
|
+
modal.appendChild(dupWarn);
|
|
9729
|
+
}
|
|
9730
|
+
|
|
9676
9731
|
modal.appendChild(fieldRow("お名前(必須)", nameInput));
|
|
9677
9732
|
var status = el("div", { class: "modal-status" });
|
|
9678
9733
|
var cancelBtn = el("button", { class: "modal-cancel", text: "閉じる" });
|
|
@@ -9684,13 +9739,166 @@ ${deriveCapTile.toString()}
|
|
|
9684
9739
|
showModal(overlay, modal);
|
|
9685
9740
|
|
|
9686
9741
|
function showStatus(msg, isError) { status.className = "modal-status" + (isError ? " modal-status-error" : ""); status.textContent = msg; }
|
|
9687
|
-
|
|
9742
|
+
|
|
9743
|
+
// ---- 会議名の確定 ----
|
|
9744
|
+
function isFreeMeeting() { return !meetingSelect || meetingSelect.value === MEETING_NEW; }
|
|
9745
|
+
/**
|
|
9746
|
+
* 自由入力の会議名を整える。
|
|
9747
|
+
*
|
|
9748
|
+
* **NFC に寄せる。** macOS の Finder から拾った名前は濁点が分解された形(NFD)で
|
|
9749
|
+
* 来ることがあり、見た目が同じなのに別ディレクトリになる。
|
|
9750
|
+
* **既存の選択肢には触らない**——あちらはビルド時にリポジトリから読んだ実物なので、
|
|
9751
|
+
* 画面で書き換えると、その会議に入らなくなる。
|
|
9752
|
+
*/
|
|
9753
|
+
function normalizeMeeting(s) {
|
|
9754
|
+
var v = String(s == null ? "" : s);
|
|
9755
|
+
if (v.normalize) v = v.normalize("NFC");
|
|
9756
|
+
return v.trim();
|
|
9757
|
+
}
|
|
9758
|
+
function meetingValue() {
|
|
9759
|
+
if (!isFreeMeeting()) return meetingSelect.value;
|
|
9760
|
+
return meetingNewInput ? normalizeMeeting(meetingNewInput.value) : "";
|
|
9761
|
+
}
|
|
9762
|
+
/** 会議名が送れない理由(送れるなら空文字) */
|
|
9763
|
+
function meetingProblem() {
|
|
9764
|
+
var v = meetingValue();
|
|
9765
|
+
if (!v) return "会議名を選んでください。";
|
|
9766
|
+
if (!isFreeMeeting()) return "";
|
|
9767
|
+
if (v === "." || v === "..") return "会議名に . や .. は使えません。";
|
|
9768
|
+
// 先頭ドットは、作れても以後の選択肢に出ない(既存会議の列挙が隠しディレクトリを
|
|
9769
|
+
// 見ないため)。次の投入者が同じ会議を選べず表記ゆれの二重化になるので、最初から断る
|
|
9770
|
+
if (v.charAt(0) === ".") return "会議名を . から始めることはできません。";
|
|
9771
|
+
if (v.indexOf("/") !== -1 || v.indexOf(BACKSLASH) !== -1) {
|
|
9772
|
+
return "会議名にスラッシュや円記号(パスの区切り)は使えません。1つの階層の名前にしてください。";
|
|
9773
|
+
}
|
|
9774
|
+
return "";
|
|
9775
|
+
}
|
|
9776
|
+
function refreshMeetingError() {
|
|
9777
|
+
if (!meetingError) return;
|
|
9778
|
+
// **開いた直後に赤くしない。** 未入力は「まだ書いていない」だけで、間違いではない。
|
|
9779
|
+
// 打った内容が使えないときだけ理由を出す(未入力は送信ボタンが無効なことで伝わる)
|
|
9780
|
+
var typed = isFreeMeeting() && meetingNewInput && normalizeMeeting(meetingNewInput.value);
|
|
9781
|
+
var msg = typed ? meetingProblem() : "";
|
|
9782
|
+
meetingError.textContent = msg;
|
|
9783
|
+
meetingError.hidden = !msg;
|
|
9784
|
+
}
|
|
9785
|
+
/**
|
|
9786
|
+
* ファイル名に既存の会議名が入っていたら、その会議を選ぶ。
|
|
9787
|
+
* 一致が複数あるときは長いほうを採る(「定例」と「開発定例」なら後者が意図に近い)。
|
|
9788
|
+
*/
|
|
9789
|
+
function autoPickMeeting(names) {
|
|
9790
|
+
if (!meetingSelect || meetingTouched || meetingSelect.value) return;
|
|
9791
|
+
var best = null;
|
|
9792
|
+
meetingList.forEach(function (m) {
|
|
9793
|
+
var seg = String(m.path).split("/").pop();
|
|
9794
|
+
if (!seg) return;
|
|
9795
|
+
for (var i = 0; i < names.length; i++) {
|
|
9796
|
+
if (String(names[i]).indexOf(seg) !== -1) {
|
|
9797
|
+
if (!best || seg.length > best.seg.length) best = { seg: seg, path: m.path };
|
|
9798
|
+
return;
|
|
9799
|
+
}
|
|
9800
|
+
}
|
|
9801
|
+
});
|
|
9802
|
+
if (best) meetingSelect.value = best.path;
|
|
9803
|
+
}
|
|
9804
|
+
|
|
9805
|
+
// ---- 会議日(ファイルごと)----
|
|
9806
|
+
function pad2(n) { return ("0" + n).slice(-2); }
|
|
9807
|
+
function todayIso() {
|
|
9808
|
+
var d = new Date();
|
|
9809
|
+
return d.getFullYear() + "-" + pad2(d.getMonth() + 1) + "-" + pad2(d.getDate());
|
|
9810
|
+
}
|
|
9811
|
+
function validYmd(y, m, d) {
|
|
9812
|
+
var mm = Number(m), dd = Number(d);
|
|
9813
|
+
return Number(y) >= 2000 && mm >= 1 && mm <= 12 && dd >= 1 && dd <= 31;
|
|
9814
|
+
}
|
|
9815
|
+
/**
|
|
9816
|
+
* ファイル名から会議日を推す(推せなければ null)。
|
|
9817
|
+
* 区切りつき(2026-01-20 / 2026年1月20日)→ 8桁の連続(20260120)の順に試す。
|
|
9818
|
+
*
|
|
9819
|
+
* **区切りつきを先に見る。** フラット置き時代の投入分は「投入日8桁-元名」の形で、
|
|
9820
|
+
* 元名の側に本当の会議日が区切りつきで入っていることがある
|
|
9821
|
+
* (実例: 20260819-2026-01-20__デイリースクラム….txt)。8桁を先に採ると
|
|
9822
|
+
* 投入日のほうを会議日として推してしまい、しかも「推定成功」の見た目になる。
|
|
9823
|
+
*/
|
|
9824
|
+
function guessMeetingDate(name) {
|
|
9825
|
+
// **変数を s・p・ext・src・internal と名付けない。** fleet-passthrough の走査は
|
|
9826
|
+
// その名前に続くプロパティを「画面が読む fleet のフィールド」とみなすので、
|
|
9827
|
+
// 文字列メソッドを呼んだだけで誤検知になる
|
|
9828
|
+
var nm = String(name || "");
|
|
9829
|
+
var sp = nm.match(/(20\\d\\d)[-_./年]\\s?(\\d{1,2})[-_./月]\\s?(\\d{1,2})[日]?/);
|
|
9830
|
+
if (sp && validYmd(sp[1], sp[2], sp[3])) return sp[1] + "-" + pad2(Number(sp[2])) + "-" + pad2(Number(sp[3]));
|
|
9831
|
+
var m8 = nm.match(/20\\d{6}/);
|
|
9832
|
+
if (m8 && validYmd(m8[0].slice(0, 4), m8[0].slice(4, 6), m8[0].slice(6, 8))) {
|
|
9833
|
+
return m8[0].slice(0, 4) + "-" + m8[0].slice(4, 6) + "-" + m8[0].slice(6, 8);
|
|
9834
|
+
}
|
|
9835
|
+
return null;
|
|
9836
|
+
}
|
|
9837
|
+
function compactDate(iso) { return String(iso || "").split("-").join(""); }
|
|
9838
|
+
|
|
9839
|
+
// ---- すでに議事録がある日 ----
|
|
9840
|
+
function selectedMeeting() {
|
|
9841
|
+
var v = meetingValue();
|
|
9842
|
+
if (!v) return null;
|
|
9843
|
+
for (var i = 0; i < meetingList.length; i++) if (meetingList[i].path === v) return meetingList[i];
|
|
9844
|
+
return null;
|
|
9845
|
+
}
|
|
9846
|
+
function dupEntries() {
|
|
9847
|
+
var m = selectedMeeting();
|
|
9848
|
+
if (!m || !m.minuteDates || !m.minuteDates.length) return [];
|
|
9849
|
+
return selectedFiles.filter(function (e) {
|
|
9850
|
+
return e.date && m.minuteDates.indexOf(compactDate(e.date)) !== -1;
|
|
9851
|
+
});
|
|
9852
|
+
}
|
|
9853
|
+
function refreshDupWarn() {
|
|
9854
|
+
if (!dupWarn) return;
|
|
9855
|
+
var dups = dupEntries();
|
|
9856
|
+
dupWarn.hidden = !dups.length;
|
|
9857
|
+
dupWarn.textContent = dups.length
|
|
9858
|
+
? "⚠ この日付には既に議事録があります。ファイルは保存されますが、議事録の自動生成は行われません: "
|
|
9859
|
+
+ dups.map(function (e) { return e.file.name; }).join(", ")
|
|
9860
|
+
: "";
|
|
9861
|
+
}
|
|
9862
|
+
|
|
9863
|
+
function update() {
|
|
9864
|
+
refreshMeetingError();
|
|
9865
|
+
refreshDupWarn();
|
|
9866
|
+
var ok = !!(nameInput.value.trim() && selectedFiles.length);
|
|
9867
|
+
// 文字起こしは会議名が確定し、各ファイルの日付が入っているときだけ送れる
|
|
9868
|
+
if (isT) {
|
|
9869
|
+
ok = ok && !meetingProblem() && selectedFiles.every(function (e) { return !!e.date; });
|
|
9870
|
+
}
|
|
9871
|
+
submitBtn.disabled = !ok;
|
|
9872
|
+
}
|
|
9688
9873
|
function renderFiles() {
|
|
9689
9874
|
fileListEl.textContent = "";
|
|
9690
|
-
selectedFiles.forEach(function (
|
|
9875
|
+
selectedFiles.forEach(function (entry, idx) {
|
|
9876
|
+
var chip = el("span", { class: "file-chip" + (isT && !entry.guessed ? " is-unguessed" : "") });
|
|
9877
|
+
chip.appendChild(el("span", { text: entry.file.name }));
|
|
9878
|
+
if (isT) {
|
|
9879
|
+
// 推せなかったファイルは当日を仮置きしてある。**そのまま送られないよう印を出す**
|
|
9880
|
+
var note = entry.guessed ? null : el("span", { class: "file-chip-guess", text: "日付を確認" });
|
|
9881
|
+
var dateInput = el("input", { type: "date", class: "file-chip-date", "aria-label": entry.file.name + " の会議日" });
|
|
9882
|
+
dateInput.value = entry.date;
|
|
9883
|
+
// **クリックがドロップゾーンへ抜けないようにする。** 抜けるとファイル選択が
|
|
9884
|
+
// 開き、日付を直そうとしただけでダイアログが出る
|
|
9885
|
+
dateInput.addEventListener("click", function (e) { e.stopPropagation(); });
|
|
9886
|
+
dateInput.addEventListener("change", function () {
|
|
9887
|
+
entry.date = dateInput.value;
|
|
9888
|
+
entry.guessed = true; // 人が触った日付は「確認済み」——注意を出し続けない
|
|
9889
|
+
// **ここで renderFiles() を呼ばない。** チップごと作り直すと入力欄が
|
|
9890
|
+
// 差し替わり、日付を打っている途中でフォーカスが飛ぶ
|
|
9891
|
+
chip.className = "file-chip";
|
|
9892
|
+
if (note && note.parentNode) note.parentNode.removeChild(note);
|
|
9893
|
+
update();
|
|
9894
|
+
});
|
|
9895
|
+
chip.appendChild(dateInput);
|
|
9896
|
+
if (note) chip.appendChild(note);
|
|
9897
|
+
}
|
|
9691
9898
|
var x = el("button", { class: "file-chip-x", type: "button", text: "×" });
|
|
9692
9899
|
x.addEventListener("click", function (e) { e.stopPropagation(); selectedFiles.splice(idx, 1); renderFiles(); update(); });
|
|
9693
|
-
|
|
9900
|
+
chip.appendChild(x);
|
|
9901
|
+
fileListEl.appendChild(chip);
|
|
9694
9902
|
});
|
|
9695
9903
|
}
|
|
9696
9904
|
function isAllowed(f) {
|
|
@@ -9702,9 +9910,16 @@ ${deriveCapTile.toString()}
|
|
|
9702
9910
|
}
|
|
9703
9911
|
function addFiles(list) {
|
|
9704
9912
|
var rejected = [];
|
|
9913
|
+
var addedNames = [];
|
|
9705
9914
|
Array.prototype.slice.call(list || []).forEach(function (f) {
|
|
9706
|
-
if (isAllowed(f))
|
|
9915
|
+
if (!isAllowed(f)) { rejected.push(f.name || "(名前なし)"); return; }
|
|
9916
|
+
var guess = isT ? guessMeetingDate(f.name) : null;
|
|
9917
|
+
// 推せなければ当日を仮置きする。**空のままにしない**——日付が空の入力欄は
|
|
9918
|
+
// 「入れなくてよい」に見えるが、実際は送れない(送信ボタンが無効なだけ)
|
|
9919
|
+
selectedFiles.push({ file: f, date: isT ? (guess || todayIso()) : "", guessed: !!guess });
|
|
9920
|
+
addedNames.push(f.name || "");
|
|
9707
9921
|
});
|
|
9922
|
+
if (isT && addedNames.length) autoPickMeeting(addedNames);
|
|
9708
9923
|
renderFiles();
|
|
9709
9924
|
update();
|
|
9710
9925
|
if (rejected.length) showStatus("⚠ 対応していない形式のため追加できませんでした: " + rejected.join(", ") + "(対応形式: " + allowedLabel + ")", true);
|
|
@@ -9716,24 +9931,41 @@ ${deriveCapTile.toString()}
|
|
|
9716
9931
|
dropzone.addEventListener("dragleave", function () { dropzone.classList.remove("dragover"); });
|
|
9717
9932
|
dropzone.addEventListener("drop", function (e) { e.preventDefault(); dropzone.classList.remove("dragover"); addFiles(e.dataTransfer.files); });
|
|
9718
9933
|
nameInput.addEventListener("input", update);
|
|
9934
|
+
if (meetingSelect) {
|
|
9935
|
+
meetingSelect.addEventListener("change", function () {
|
|
9936
|
+
meetingTouched = true;
|
|
9937
|
+
meetingNewInput.hidden = meetingSelect.value !== MEETING_NEW;
|
|
9938
|
+
// 選択肢に戻したら打ちかけの名前は捨てる(隠れた入力が送られるのを防ぐ)
|
|
9939
|
+
if (meetingSelect.value !== MEETING_NEW) meetingNewInput.value = "";
|
|
9940
|
+
update();
|
|
9941
|
+
});
|
|
9942
|
+
}
|
|
9943
|
+
if (meetingNewInput) meetingNewInput.addEventListener("input", update);
|
|
9719
9944
|
|
|
9720
9945
|
// 閉じる経路(Escape・背景クリック)は makeModal 側に用意されている
|
|
9721
9946
|
cancelBtn.addEventListener("click", function () { mk.close(); });
|
|
9722
|
-
submitBtn.addEventListener("click", function () {
|
|
9947
|
+
submitBtn.addEventListener("click", function () {
|
|
9948
|
+
// 全ファイルが「議事録のある日」なら、成功メッセージも「生成はされない」側に寄せる。
|
|
9949
|
+
// ✓ の直後に「数分後に議事録が自動生成され…」と出すと、待っても出ない
|
|
9950
|
+
var allDup = isT && selectedFiles.length > 0 && dupEntries().length === selectedFiles.length;
|
|
9951
|
+
submitIntake(kind, selectedFiles, nameInput.value, status, submitBtn, isT ? meetingValue() : null, allDup);
|
|
9952
|
+
});
|
|
9723
9953
|
}
|
|
9724
|
-
async function submitIntake(kind,
|
|
9954
|
+
async function submitIntake(kind, entries, submitter, status, submitBtn, meeting, allDup) {
|
|
9725
9955
|
var intake = SITE.intake;
|
|
9956
|
+
var isT = kind === "transcript";
|
|
9726
9957
|
function setStatus(msg, isError) { status.className = "modal-status" + (isError ? " modal-status-error" : ""); status.textContent = msg; }
|
|
9727
9958
|
if (!submitter || !submitter.trim()) { setStatus("⚠ お名前を入力してください。", true); return; }
|
|
9728
|
-
if (!
|
|
9959
|
+
if (!entries || !entries.length) { setStatus("⚠ ファイルを選択してください。", true); return; }
|
|
9960
|
+
if (isT && !meeting) { setStatus("⚠ 会議名を選んでください。", true); return; }
|
|
9729
9961
|
submitBtn.disabled = true;
|
|
9730
9962
|
var pfx = intake.url + (intake.url.indexOf("?") === -1 ? "?" : "&");
|
|
9731
9963
|
function fail(msg) { setStatus("✗ " + msg, true); submitBtn.disabled = false; }
|
|
9732
9964
|
try {
|
|
9733
9965
|
var uploaded = [];
|
|
9734
|
-
for (var i = 0; i <
|
|
9735
|
-
var f =
|
|
9736
|
-
setStatus("アップロード中… (" + (i + 1) + "/" +
|
|
9966
|
+
for (var i = 0; i < entries.length; i++) {
|
|
9967
|
+
var f = entries[i].file;
|
|
9968
|
+
setStatus("アップロード中… (" + (i + 1) + "/" + entries.length + ") " + f.name, false);
|
|
9737
9969
|
var pres;
|
|
9738
9970
|
try {
|
|
9739
9971
|
pres = await fetch(pfx + "op=presign&t=" + encodeURIComponent(intake.token), {
|
|
@@ -9747,20 +9979,27 @@ ${deriveCapTile.toString()}
|
|
|
9747
9979
|
put = await fetch(pres.j.url, { method: "PUT", headers: { "content-type": f.type || "application/octet-stream" }, body: f });
|
|
9748
9980
|
} catch (net2) { return fail("ファイルのアップロードに失敗しました(通信エラー)。もう一度お試しください。"); }
|
|
9749
9981
|
if (!put.ok) return fail("ファイルのアップロードに失敗しました(" + put.status + ")。ファイルのサイズ・形式をご確認ください。");
|
|
9750
|
-
|
|
9982
|
+
var up = { key: pres.j.key, filename: f.name };
|
|
9983
|
+
// 会議日は YYYYMMDD で送る(そのまま日付ディレクトリの名前になる)
|
|
9984
|
+
if (isT) up.date = String(entries[i].date || "").split("-").join("");
|
|
9985
|
+
uploaded.push(up);
|
|
9751
9986
|
}
|
|
9752
9987
|
setStatus("取り込み中…", false);
|
|
9753
9988
|
var sub;
|
|
9754
9989
|
try {
|
|
9990
|
+
var payload = { projectKey: intake.projectKey, kind: kind, files: uploaded, submitter: submitter.trim() };
|
|
9991
|
+
if (isT) payload.meeting = meeting;
|
|
9755
9992
|
sub = await fetch(pfx + "op=submit&t=" + encodeURIComponent(intake.token), {
|
|
9756
9993
|
method: "POST", headers: { "content-type": "application/json" },
|
|
9757
|
-
body: JSON.stringify(
|
|
9994
|
+
body: JSON.stringify(payload),
|
|
9758
9995
|
}).then(function (r) { return r.json().then(function (j) { return { ok: r.ok, j: j }; }); });
|
|
9759
9996
|
} catch (net3) { return fail("サーバーに接続できませんでした。もう一度お試しください。"); }
|
|
9760
9997
|
if (!sub.ok) return fail((sub.j && sub.j.message) || "取り込みに失敗しました。時間をおいて再度お試しください。");
|
|
9761
9998
|
status.className = "modal-status";
|
|
9762
9999
|
status.textContent = "";
|
|
9763
|
-
status.appendChild(el("div", { class: "modal-ok", text:
|
|
10000
|
+
status.appendChild(el("div", { class: "modal-ok", text: allDup
|
|
10001
|
+
? "✓ 送信しました。保存されました(この日付には既に議事録があるため、議事録の自動生成は行われません)"
|
|
10002
|
+
: "✓ 送信しました。数分後に" + (isT ? "議事録が自動生成され" : "内容がMarkdownに変換されて") + "反映されます。" }));
|
|
9764
10003
|
var paths = (sub.j && sub.j.committed) || [];
|
|
9765
10004
|
if (paths.length && sub.j.repo) {
|
|
9766
10005
|
var br = SITE.branch || "main";
|
package/package.json
CHANGED