@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 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
- function update() { submitBtn.disabled = !(nameInput.value.trim() && selectedFiles.length); }
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 (f, idx) {
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
- fileListEl.appendChild(el("span", { class: "file-chip" }, [el("span", { text: f.name }), x]));
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)) selectedFiles.push(f); else rejected.push(f.name || "(名前なし)");
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 () { submitIntake(kind, selectedFiles, nameInput.value, status, submitBtn); });
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, files, submitter, status, submitBtn) {
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 (!files || !files.length) { setStatus("⚠ ファイルを選択してください。", true); return; }
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 < files.length; i++) {
9735
- var f = files[i];
9736
- setStatus("アップロード中… (" + (i + 1) + "/" + files.length + ") " + f.name, false);
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
- uploaded.push({ key: pres.j.key, filename: f.name });
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({ projectKey: intake.projectKey, kind: kind, files: uploaded, submitter: submitter.trim() }),
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: "✓ 送信しました。数分後に" + (kind === "transcript" ? "議事録が自動生成され" : "内容がMarkdownに変換されて") + "反映されます。" }));
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@takagaki/cortex-decisions-viewer",
3
- "version": "0.12.9",
3
+ "version": "0.12.10",
4
4
  "description": "Cortexの意思決定記録(Decisions/*.md)を静的サイトにビルドして閲覧する。各レコードに「Edit on GitHub」リンクを付与する。",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,