From b24aa19ad22f59556fc7bbb63c58836799239588 Mon Sep 17 00:00:00 2001 From: tanaka takashi Date: Sun, 13 Sep 2026 14:36:46 +0900 Subject: fix(ui): スマホでセッションを削除できるようにする MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit セッション削除ボタンが通常時はdisplay:noneで、li:hover時だけ表示されていたため、持続的なhoverを持たないスマートフォンでは安定して操作できなかった。モバイル幅、hover非対応、coarse pointer環境ではボタンを常時表示し、44x44pxのタップ領域とtouch-actionを設定する。 表示修正後もタップ後の処理がネイティブwindow.confirmと複数のclickリスナーに依存していたため、削除とセッション選択をsession-list上の単一イベント委譲へ統合する。削除ボタンを先に判定して行選択とサイドバー閉鎖へ伝播しないようにした。 ネイティブ確認ダイアログを既存modalスタイルに沿ったアプリ内確認ダイアログへ置き換える。キャンセル、背景タップ、Escape、フォーカス復帰に対応し、DELETE実行中の二重送信を防止する。通信失敗はネイティブalertではなくダイアログ内のaria-live領域へ表示する。 削除ボタンへbutton typeとaria-labelを明示し、確認ダイアログへdialog semanticsと日英のキャンセル文言を追加する。CHANGELOGと、タッチ向けCSSのカスケード順・サイズ・ネイティブconfirm非依存を確認する回帰テストも更新する。 Validation: 150 tests passed; ruff check passed; node --check src/ollama_web/static/app.js passed; git diff --check passed. --- src/ollama_web/i18n/messages/en.json | 1 + src/ollama_web/i18n/messages/ja.json | 1 + src/ollama_web/static/app.js | 73 ++++++++++++++++++++++++++++++++---- src/ollama_web/static/style.css | 30 +++++++++++++++ src/ollama_web/templates/index.html | 21 ++++++++++- 5 files changed, 117 insertions(+), 9 deletions(-) (limited to 'src') diff --git a/src/ollama_web/i18n/messages/en.json b/src/ollama_web/i18n/messages/en.json index 878e53c..e20fbd8 100644 --- a/src/ollama_web/i18n/messages/en.json +++ b/src/ollama_web/i18n/messages/en.json @@ -5,6 +5,7 @@ "clear": "Clear", "save": "Save", "delete": "Delete", + "cancel": "Cancel", "close": "Close", "copy": "Copy" }, diff --git a/src/ollama_web/i18n/messages/ja.json b/src/ollama_web/i18n/messages/ja.json index b39e85e..36094e5 100644 --- a/src/ollama_web/i18n/messages/ja.json +++ b/src/ollama_web/i18n/messages/ja.json @@ -5,6 +5,7 @@ "clear": "クリア", "save": "保存", "delete": "削除", + "cancel": "キャンセル", "close": "閉じる", "copy": "コピー" }, diff --git a/src/ollama_web/static/app.js b/src/ollama_web/static/app.js index 753ee99..a498e37 100644 --- a/src/ollama_web/static/app.js +++ b/src/ollama_web/static/app.js @@ -15,6 +15,10 @@ const newSessionBtn = document.getElementById("new-session-btn"); const menuBtn = document.getElementById("menu-btn"); const sidebar = document.getElementById("sidebar"); const sidebarBackdrop = document.getElementById("sidebar-backdrop"); +const deleteSessionModal = document.getElementById("delete-session-modal"); +const deleteSessionCancelBtn = document.getElementById("delete-session-cancel"); +const deleteSessionConfirmBtn = document.getElementById("delete-session-confirm"); +const deleteSessionStatus = document.getElementById("delete-session-status"); const TOOLS = window.TOOLS || []; let currentSessionId = null; @@ -23,6 +27,8 @@ let pendingFiles = []; let abortController = null; let currentCapabilities = new Set(); let stickToBottom = true; +let pendingDeleteSessionId = null; +let deleteSessionTrigger = null; function updateThinkToggleState() { if (!thinkToggle) return; @@ -308,15 +314,12 @@ function renderSessionList(sessions) { title.className = "title"; title.textContent = s.title || s.id; const del = document.createElement("button"); + del.type = "button"; del.className = "delete"; del.textContent = t("common.delete"); del.title = t("chat.delete_session"); - del.addEventListener("click", (e) => { - e.stopPropagation(); - deleteSession(s.id); - }); + del.setAttribute("aria-label", t("chat.delete_session")); li.append(title, del); - li.addEventListener("click", () => selectSession(s.id)); sessionListEl.appendChild(li); } } @@ -334,8 +337,28 @@ async function createSession() { } } +function openDeleteSessionDialog(id, trigger) { + pendingDeleteSessionId = id; + deleteSessionTrigger = trigger; + deleteSessionStatus.textContent = ""; + deleteSessionConfirmBtn.disabled = false; + deleteSessionModal.classList.add("open"); + deleteSessionModal.setAttribute("aria-hidden", "false"); + deleteSessionConfirmBtn.focus(); +} + +function closeDeleteSessionDialog({ restoreFocus = true } = {}) { + deleteSessionModal.classList.remove("open"); + deleteSessionModal.setAttribute("aria-hidden", "true"); + pendingDeleteSessionId = null; + deleteSessionStatus.textContent = ""; + if (restoreFocus && deleteSessionTrigger?.isConnected) { + deleteSessionTrigger.focus(); + } + deleteSessionTrigger = null; +} + async function deleteSession(id) { - if (!confirm(t("chat.delete_confirm"))) return; try { const res = await csrfFetch(`/api/sessions/${id}`, { method: "DELETE" }); if (!res.ok) throw new Error(res.statusText); @@ -348,9 +371,11 @@ async function deleteSession(id) { updateTokenGauge(0, 0); } await loadSessions(); + return true; } catch (err) { console.error("failed to delete session", err); - alert(t("chat.delete_failed")); + deleteSessionStatus.textContent = t("chat.delete_failed"); + return false; } } @@ -583,13 +608,45 @@ sidebarBackdrop.addEventListener("click", () => { }); sessionListEl.addEventListener("click", (e) => { + const deleteButton = e.target.closest(".delete"); + if (deleteButton) { + e.preventDefault(); + e.stopPropagation(); + const li = deleteButton.closest("li"); + if (li?.dataset.id) openDeleteSessionDialog(li.dataset.id, deleteButton); + return; + } const li = e.target.closest("li"); - if (li && li.dataset.id && !e.target.closest(".delete")) { + if (li?.dataset.id) { + selectSession(li.dataset.id); sidebar.classList.remove("open"); sidebarBackdrop.classList.remove("active"); } }); +deleteSessionCancelBtn.addEventListener("click", () => closeDeleteSessionDialog()); + +deleteSessionConfirmBtn.addEventListener("click", async () => { + const id = pendingDeleteSessionId; + if (!id || deleteSessionConfirmBtn.disabled) return; + deleteSessionConfirmBtn.disabled = true; + deleteSessionModal.setAttribute("aria-busy", "true"); + const deleted = await deleteSession(id); + deleteSessionModal.removeAttribute("aria-busy"); + deleteSessionConfirmBtn.disabled = false; + if (deleted) closeDeleteSessionDialog({ restoreFocus: false }); +}); + +deleteSessionModal.addEventListener("click", (e) => { + if (e.target === deleteSessionModal) closeDeleteSessionDialog(); +}); + +document.addEventListener("keydown", (e) => { + if (e.key === "Escape" && deleteSessionModal.classList.contains("open")) { + closeDeleteSessionDialog(); + } +}); + modelSelect.addEventListener("change", loadModelCapabilities); async function send() { diff --git a/src/ollama_web/static/style.css b/src/ollama_web/static/style.css index 2b6d4bc..beb65d1 100644 --- a/src/ollama_web/static/style.css +++ b/src/ollama_web/static/style.css @@ -282,6 +282,20 @@ body { display: inline; } +/* Touch devices cannot reliably reveal hover-only controls. Keep the action + visible and large enough to tap without changing the desktop interaction. */ +@media (max-width: 768px), (hover: none), (pointer: coarse) { + #session-list li .delete { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 44px; + min-height: 44px; + flex-shrink: 0; + touch-action: manipulation; + } +} + #session-list li.empty { color: var(--muted); font-size: 13px; @@ -692,6 +706,16 @@ button#send-btn { padding: 0 20px; } flex-direction: column; } +.confirm-modal-content { + width: min(420px, 100%); +} + +.delete-session-status { + min-height: 20px; + color: var(--danger); + font-size: 13px; +} + .modal-header { display: flex; align-items: center; @@ -755,6 +779,12 @@ button#send-btn { padding: 0 20px; } background: transparent; } +.modal-footer button.danger { + background: var(--danger); + border-color: var(--danger); + color: #fff; +} + .mcp-server-card { background: var(--bg); border: 1px solid var(--border); diff --git a/src/ollama_web/templates/index.html b/src/ollama_web/templates/index.html index 54202e3..86f1ee1 100644 --- a/src/ollama_web/templates/index.html +++ b/src/ollama_web/templates/index.html @@ -76,6 +76,25 @@ + +