diff options
| author | tanaka takashi <isazaka@gmail.com> | 2026-06-24 00:19:26 +0900 |
|---|---|---|
| committer | tanaka takashi <isazaka@gmail.com> | 2026-06-24 00:19:26 +0900 |
| commit | 16a8bfebde12e2e3fa50b2a1384091158fdabe91 (patch) | |
| tree | ab9e64216510560204e38dbe464bca892dc1713a /src/ollama_web/static/app.js | |
| parent | 5d9fc9c9c976cb6fc7d0b8060abcd2860ebf8456 (diff) | |
機能追加(セッション、左ペインでのセッション管理、ファイル取得ツール、pdfテキスト化ツール)
Diffstat (limited to 'src/ollama_web/static/app.js')
| -rw-r--r-- | src/ollama_web/static/app.js | 231 |
1 files changed, 217 insertions, 14 deletions
diff --git a/src/ollama_web/static/app.js b/src/ollama_web/static/app.js index 845a71f..339030a 100644 --- a/src/ollama_web/static/app.js +++ b/src/ollama_web/static/app.js @@ -8,12 +8,17 @@ const modelSelect = document.getElementById("model-select"); const thinkToggle = document.getElementById("think-toggle"); const clearBtn = document.getElementById("clear-btn"); const cancelBtn = document.getElementById("cancel-btn"); +const fileInput = document.getElementById("file-input"); +const attachmentsEl = document.getElementById("attachments"); +const sessionListEl = document.getElementById("session-list"); +const newSessionBtn = document.getElementById("new-session-btn"); -let messages = []; +let currentSessionId = null; +let currentSession = null; +let pendingFiles = []; let abortController = null; function simpleMarkdownToHtml(text) { - // Render plain text safely as HTML without external dependencies. const el = document.createElement("div"); el.setAttribute("style", "white-space:pre-wrap"); el.textContent = text; @@ -115,19 +120,211 @@ function parseSseEvent(chunk) { return events; } +async function loadSessions() { + try { + const res = await fetch("/api/sessions"); + if (!res.ok) throw new Error(res.statusText); + const data = await res.json(); + renderSessionList(data.sessions || []); + } catch (err) { + console.error("failed to load sessions", err); + renderSessionList([]); + } +} + +function renderSessionList(sessions) { + sessionListEl.innerHTML = ""; + if (!sessions.length) { + const li = document.createElement("li"); + li.className = "empty"; + li.textContent = "セッションがありません"; + sessionListEl.appendChild(li); + return; + } + for (const s of sessions) { + const li = document.createElement("li"); + li.dataset.id = s.id; + if (s.id === currentSessionId) li.classList.add("active"); + const title = document.createElement("span"); + title.className = "title"; + title.textContent = s.title || s.id; + const del = document.createElement("button"); + del.className = "delete"; + del.textContent = "削除"; + del.title = "セッションを削除"; + del.addEventListener("click", (e) => { + e.stopPropagation(); + deleteSession(s.id); + }); + li.append(title, del); + li.addEventListener("click", () => selectSession(s.id)); + sessionListEl.appendChild(li); + } +} + +async function createSession() { + try { + const res = await fetch("/api/sessions", { method: "POST" }); + if (!res.ok) throw new Error(res.statusText); + const session = await res.json(); + await loadSessions(); + await selectSession(session.id); + } catch (err) { + console.error("failed to create session", err); + alert("セッションの作成に失敗しました"); + } +} + +async function deleteSession(id) { + if (!confirm("このセッションを削除しますか?")) return; + try { + const res = await fetch(`/api/sessions/${id}`, { method: "DELETE" }); + if (!res.ok) throw new Error(res.statusText); + if (currentSessionId === id) { + currentSessionId = null; + currentSession = null; + chatEl.innerHTML = ""; + pendingFiles = []; + renderAttachments(); + } + await loadSessions(); + } catch (err) { + console.error("failed to delete session", err); + alert("セッションの削除に失敗しました"); + } +} + +async function selectSession(id) { + try { + const res = await fetch(`/api/sessions/${id}`); + if (!res.ok) throw new Error(res.statusText); + const session = await res.json(); + currentSessionId = session.id; + currentSession = session; + renderSession(); + renderSessionList((await (await fetch("/api/sessions")).json()).sessions || []); + } catch (err) { + console.error("failed to select session", err); + } +} + +function renderSession() { + chatEl.innerHTML = ""; + if (!currentSession) return; + const msgs = currentSession.messages || []; + for (const m of msgs) { + if (m.role === "user") { + const body = addMessage("user").body; + body.innerHTML = renderMarkdown(m.content); + highlightIn(body); + if (m.attachments && m.attachments.length) { + const info = document.createElement("div"); + info.style.fontSize = "12px"; + info.style.color = "var(--muted)"; + info.style.marginTop = "6px"; + const names = m.attachments + .map((fid) => { + const f = (currentSession.files || []).find((x) => x.id === fid); + return f ? f.name : fid; + }) + .join(", "); + info.textContent = `添付: ${names}`; + body.appendChild(info); + } + } else if (m.role === "assistant") { + const body = addMessage("assistant").body; + body.innerHTML = renderMarkdown(m.content); + highlightIn(body); + } + } +} + +function renderAttachments() { + attachmentsEl.innerHTML = ""; + for (const f of pendingFiles) { + const chip = document.createElement("span"); + chip.className = "attachment-chip"; + chip.textContent = f.name; + const remove = document.createElement("span"); + remove.className = "remove"; + remove.textContent = "×"; + remove.addEventListener("click", () => { + pendingFiles = pendingFiles.filter((x) => x !== f); + renderAttachments(); + }); + chip.appendChild(remove); + attachmentsEl.appendChild(chip); + } +} + +async function uploadFiles(files) { + if (!currentSessionId) { + await createSession(); + } + if (!currentSessionId) return; + + const form = new FormData(); + for (const file of files) { + form.append("files", file); + } + try { + const res = await fetch(`/api/sessions/${currentSessionId}/files`, { + method: "POST", + body: form, + }); + if (!res.ok) throw new Error(res.statusText); + const data = await res.json(); + for (const f of data.files || []) { + if (f.error) { + alert(`${f.name}: ${f.error}`); + continue; + } + pendingFiles.push({ id: f.id, name: f.name }); + } + renderAttachments(); + await selectSession(currentSessionId); + } catch (err) { + console.error("failed to upload files", err); + alert("ファイルのアップロードに失敗しました"); + } +} + +fileInput.addEventListener("change", () => { + if (fileInput.files && fileInput.files.length) { + uploadFiles(fileInput.files); + fileInput.value = ""; + } +}); + +newSessionBtn.addEventListener("click", createSession); + async function send() { const text = inputEl.value.trim(); - if (!text) return; - inputEl.value = ""; + if (!text && !pendingFiles.length) return; + + if (!currentSessionId) { + await createSession(); + } + if (!currentSessionId) return; + + if (text) inputEl.value = ""; sendBtn.disabled = true; cancelBtn.disabled = false; abortController = new AbortController(); - messages.push({ role: "user", content: text }); + const fileIds = pendingFiles.map((f) => f.id); const userBody = addMessage("user").body; - userBody.innerHTML = renderMarkdown(text); + let displayContent = text; + if (pendingFiles.length) { + const names = pendingFiles.map((f) => f.name).join(", "); + displayContent += (text ? "\n\n" : "") + `添付ファイル: ${names}`; + } + userBody.innerHTML = renderMarkdown(displayContent); highlightIn(userBody); + pendingFiles = []; + renderAttachments(); + const assistantMsg = addMessage("assistant"); const assistantBody = assistantMsg.body; const assistantExtras = assistantMsg.extras; @@ -139,7 +336,14 @@ async function send() { const model = modelSelect.value; const think = thinkToggle.checked; - const requestBody = JSON.stringify({ model, messages, think }); + const requestBody = JSON.stringify({ + model, + messages: text ? [{ role: "user", content: text }] : [], + think, + session_id: currentSessionId, + file_ids: fileIds, + }); + let res; try { res = await fetch("/api/chat", { @@ -169,7 +373,6 @@ async function send() { const decoder = new TextDecoder(); let buffer = ""; - // Remove the placeholder "…" as soon as real content arrives. let placeholderCleared = false; function appendDelta(content) { @@ -188,7 +391,7 @@ async function send() { if (done) break; buffer += decoder.decode(value, { stream: true }); - const endIndex = buffer.lastIndexOf("\n\n"); + const endIndex = buffer.lastIndexOf("\\n\\n"); if (endIndex === -1) continue; const chunk = buffer.slice(0, endIndex); buffer = buffer.slice(endIndex + 2); @@ -250,13 +453,12 @@ async function send() { assistantBody.textContent = "(応答なし)"; } - if (assistantText) { - messages.push({ role: "assistant", content: assistantText }); - } abortController = null; cancelBtn.disabled = true; sendBtn.disabled = false; inputEl.focus(); + + await selectSession(currentSessionId); } formEl.addEventListener("submit", (e) => { @@ -272,7 +474,6 @@ inputEl.addEventListener("keydown", (e) => { }); clearBtn.addEventListener("click", () => { - messages = []; chatEl.innerHTML = ""; }); @@ -280,4 +481,6 @@ cancelBtn.addEventListener("click", () => { if (abortController) { abortController.abort(); } -});
\ No newline at end of file +}); + +loadSessions();
\ No newline at end of file |
