summaryrefslogtreecommitdiff
path: root/src/ollama_web
diff options
context:
space:
mode:
authortanaka takashi <isazaka@gmail.com>2026-09-13 14:36:46 +0900
committertanaka takashi <isazaka@gmail.com>2026-09-13 14:36:46 +0900
commitb24aa19ad22f59556fc7bbb63c58836799239588 (patch)
tree0a5cee4357f864fc73cba37d7cd2eb23d5266385 /src/ollama_web
parentf2d24cb7d437ad099291262d7c1b021809cd66ae (diff)
fix(ui): スマホでセッションを削除できるようにする
セッション削除ボタンが通常時は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.
Diffstat (limited to 'src/ollama_web')
-rw-r--r--src/ollama_web/i18n/messages/en.json1
-rw-r--r--src/ollama_web/i18n/messages/ja.json1
-rw-r--r--src/ollama_web/static/app.js73
-rw-r--r--src/ollama_web/static/style.css30
-rw-r--r--src/ollama_web/templates/index.html21
5 files changed, 117 insertions, 9 deletions
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 @@
</form>
</div>
+ <div id="delete-session-modal" class="modal" aria-hidden="true">
+ <div class="modal-content confirm-modal-content" role="dialog" aria-modal="true"
+ aria-labelledby="delete-session-modal-title"
+ aria-describedby="delete-session-modal-message">
+ <div class="modal-header">
+ <h2 id="delete-session-modal-title">{{ t('chat.delete_session') }}</h2>
+ </div>
+ <div class="modal-body">
+ <p id="delete-session-modal-message" class="modal-help">{{ t('chat.delete_confirm') }}</p>
+ <div id="delete-session-status" class="delete-session-status" role="status"
+ aria-live="polite"></div>
+ </div>
+ <div class="modal-footer">
+ <button id="delete-session-cancel" type="button" class="secondary">{{ t('common.cancel') }}</button>
+ <button id="delete-session-confirm" type="button" class="danger">{{ t('common.delete') }}</button>
+ </div>
+ </div>
+ </div>
+
<div id="settings-modal" class="modal" aria-hidden="true">
<div class="modal-content settings-modal-content" role="dialog" aria-modal="true"
aria-labelledby="settings-modal-title">
@@ -202,4 +221,4 @@
<script src="{{ url_for('static', path='settings-actions.js') }}"></script>
<script src="{{ url_for('static', path='app.js') }}"></script>
</body>
-</html> \ No newline at end of file
+</html>