From ef0360497ce084da91ab679cec0ae3a568b85c37 Mon Sep 17 00:00:00 2001 From: dj-oyu <68707227+dj-oyu@users.noreply.github.com> Date: Sun, 15 Mar 2026 15:36:06 +0900 Subject: [PATCH] feat: add Research tab to Mini App Add a Research tab to the Telegram Mini App for managing research tasks directly from the embedded dashboard, without requiring the separate web management UI (picoclaw-launcher). - index.html: add Research tab button + panel with task list, create form, and detail view with document accordion - app.js: loadResearch(), openResearchTask(), createResearchTask(), toggleResearchDoc(), researchAction() functions - styles.css: update --tab-count default from 7 to 8 Co-Authored-By: Claude Opus 4.6 (1M context) --- pkg/miniapp/frontend/src/app.js | 194 +++++++++++++++++++++++++++- pkg/miniapp/frontend/src/styles.css | 2 +- pkg/miniapp/static/dist/app.css | 2 +- pkg/miniapp/static/dist/app.js | 137 +++++++++++++++++++- pkg/miniapp/static/index.html | 26 ++++ 5 files changed, 357 insertions(+), 4 deletions(-) diff --git a/pkg/miniapp/frontend/src/app.js b/pkg/miniapp/frontend/src/app.js index 400f57676..009d500bb 100644 --- a/pkg/miniapp/frontend/src/app.js +++ b/pkg/miniapp/frontend/src/app.js @@ -15,7 +15,7 @@ if (!window.ORCH_ENABLED) { var orchPanel = document.getElementById('orch'); if (orchTabBtn) orchTabBtn.style.display = 'none'; if (orchPanel) orchPanel.style.display = 'none'; - document.documentElement.style.setProperty('--tab-count', '6'); + document.documentElement.style.setProperty('--tab-count', '7'); } // Tab switching — re-fetch data unless SSE delivered recently @@ -42,6 +42,7 @@ tabs.forEach((tab, index) => { if (p === 'plan' && !fresh) loadPlan(); if (p === 'skills' && !fresh) loadSkills(); if (p === 'session' && !fresh) loadSession(); + if (p === 'research') loadResearch(); if (p === 'git') loadGit(); if (p === 'dev' && !fresh) loadDev(); if (p === 'config') connectLogsWs(); @@ -1337,5 +1338,196 @@ window.toggleSystemPrompt = toggleSystemPrompt; window.loadGit = loadGit; window.saveLogSnapshot = saveLogSnapshot; +// ── Research ── + +var researchCurrentTaskId = null; + +const STATUS_COLORS = { + pending: { bg: 'rgba(234,179,8,0.15)', text: '#ca8a04' }, + active: { bg: 'rgba(59,130,246,0.15)', text: '#2563eb' }, + completed: { bg: 'rgba(34,197,94,0.15)', text: '#16a34a' }, + failed: { bg: 'rgba(239,68,68,0.15)', text: '#dc2626' }, + canceled: { bg: 'rgba(107,114,128,0.15)', text: '#6b7280' }, +}; + +async function loadResearch() { + var el = document.getElementById('research-tasks'); + var loading = document.getElementById('research-loading'); + loading.classList.remove('hidden'); + el.innerHTML = ''; + try { + var resp = await fetch( + API_BASE + '/miniapp/api/research?initData=' + encodeURIComponent(initData) + ); + var tasks = await resp.json(); + loading.classList.add('hidden'); + if (!tasks || tasks.length === 0) { + el.innerHTML = '
No research tasks yet.
'; + return; + } + el.innerHTML = tasks.map(function(t) { + var sc = STATUS_COLORS[t.status] || STATUS_COLORS.pending; + return '
' + + '
' + + '' + esc(t.title) + '' + + '' + t.status + '' + + '
' + + (t.description ? '
' + esc(t.description).substring(0, 120) + '
' : '') + + '
' + + t.document_count + ' docs · ' + new Date(t.created_at).toLocaleDateString() + + '
' + + '
'; + }).join(''); + } catch (e) { + loading.classList.add('hidden'); + el.innerHTML = '
Failed to load tasks.
'; + } +} + +function esc(s) { + var d = document.createElement('div'); + d.textContent = s; + return d.innerHTML; +} + +async function openResearchTask(id) { + researchCurrentTaskId = id; + document.getElementById('research-list-view').classList.add('hidden'); + document.getElementById('research-detail-view').classList.remove('hidden'); + var el = document.getElementById('research-detail-content'); + el.innerHTML = '
Loading...
'; + try { + var resp = await fetch( + API_BASE + '/miniapp/api/research/' + id + '?initData=' + encodeURIComponent(initData) + ); + var task = await resp.json(); + var sc = STATUS_COLORS[task.status] || STATUS_COLORS.pending; + var canCancel = task.status === 'pending' || task.status === 'active'; + var canReopen = task.status === 'completed' || task.status === 'failed'; + + var html = '
' + + '
' + + '' + esc(task.title) + '' + + '' + task.status + '' + + '
'; + if (task.description) { + html += '
' + esc(task.description) + '
'; + } + html += '
' + + 'Created: ' + new Date(task.created_at).toLocaleString() + + (task.completed_at ? ' · Completed: ' + new Date(task.completed_at).toLocaleString() : '') + + '
'; + if (canCancel || canReopen) { + html += '
'; + if (canCancel) html += ''; + if (canReopen) html += ''; + html += '
'; + } + html += '
'; + + // Documents + html += '
Documents (' + task.documents.length + ')
'; + if (task.documents.length === 0) { + html += '
No documents yet.
'; + } else { + html += task.documents.map(function(d) { + return '
' + + '
' + + '#' + d.seq + '' + + '' + esc(d.title) + '' + + '' + d.doc_type + '' + + '
' + + (d.summary ? '
' + esc(d.summary) + '
' : '') + + '' + + '
'; + }).join(''); + } + el.innerHTML = html; + } catch (e) { + el.innerHTML = '
Failed to load task.
'; + } +} + +async function toggleResearchDoc(card, taskId, docId) { + var body = card.querySelector('.research-doc-body'); + if (!body) return; + if (!body.classList.contains('hidden')) { + body.classList.add('hidden'); + return; + } + body.classList.remove('hidden'); + if (body.dataset.loaded) return; + try { + var resp = await fetch( + API_BASE + '/miniapp/api/research/' + taskId + '/doc/' + docId + + '?initData=' + encodeURIComponent(initData) + ); + var data = await resp.json(); + body.dataset.loaded = '1'; + body.innerHTML = '
' +
+      esc(data.content) + '
'; + } catch (e) { + body.innerHTML = '
Failed to load document.
'; + } +} + +async function researchAction(taskId, action) { + try { + await fetch( + API_BASE + '/miniapp/api/research/' + taskId + '?initData=' + encodeURIComponent(initData), + { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: action }) } + ); + openResearchTask(taskId); + } catch (e) { + // ignore + } +} + +function showResearchList() { + document.getElementById('research-detail-view').classList.add('hidden'); + document.getElementById('research-list-view').classList.remove('hidden'); + researchCurrentTaskId = null; + loadResearch(); +} + +function showNewTaskForm() { + document.getElementById('research-new-form').classList.remove('hidden'); + document.getElementById('research-title').focus(); +} + +function hideNewTaskForm() { + document.getElementById('research-new-form').classList.add('hidden'); + document.getElementById('research-title').value = ''; + document.getElementById('research-desc').value = ''; +} + +async function createResearchTask() { + var title = document.getElementById('research-title').value.trim(); + var desc = document.getElementById('research-desc').value.trim(); + if (!title) return; + try { + await fetch( + API_BASE + '/miniapp/api/research?initData=' + encodeURIComponent(initData), + { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ title: title, description: desc }) } + ); + hideNewTaskForm(); + loadResearch(); + } catch (e) { + // ignore + } +} + +window.showNewTaskForm = showNewTaskForm; +window.hideNewTaskForm = hideNewTaskForm; +window.createResearchTask = createResearchTask; +window.openResearchTask = openResearchTask; +window.toggleResearchDoc = toggleResearchDoc; +window.researchAction = researchAction; +window.showResearchList = showResearchList; + diff --git a/pkg/miniapp/frontend/src/styles.css b/pkg/miniapp/frontend/src/styles.css index 538f1715d..7847478d4 100644 --- a/pkg/miniapp/frontend/src/styles.css +++ b/pkg/miniapp/frontend/src/styles.css @@ -76,7 +76,7 @@ top: 2px; bottom: 2px; left: 2px; - width: calc(100% / var(--tab-count, 7) - 2px); + width: calc(100% / var(--tab-count, 8) - 2px); border-radius: 8px; background: var(--tab-pill-bg); box-shadow: 0 0.5px 2px rgba(0,0,0,0.12), 0 0.5px 1px rgba(0,0,0,0.08); diff --git a/pkg/miniapp/static/dist/app.css b/pkg/miniapp/static/dist/app.css index b871d83f5..27b0f8337 100644 --- a/pkg/miniapp/static/dist/app.css +++ b/pkg/miniapp/static/dist/app.css @@ -76,7 +76,7 @@ body { .tab-indicator { position: absolute; - width: calc(100% / var(--tab-count, 7) - 2px); + width: calc(100% / var(--tab-count, 8) - 2px); background: var(--tab-pill-bg); z-index: 0; border-radius: 8px; diff --git a/pkg/miniapp/static/dist/app.js b/pkg/miniapp/static/dist/app.js index ba4140aac..de2e00f8e 100644 --- a/pkg/miniapp/static/dist/app.js +++ b/pkg/miniapp/static/dist/app.js @@ -91,7 +91,7 @@ orchTabBtn.style.display = "none"; if (orchPanel) orchPanel.style.display = "none"; - document.documentElement.style.setProperty("--tab-count", "6"); + document.documentElement.style.setProperty("--tab-count", "7"); } var orchTabBtn; var orchPanel; @@ -116,6 +116,8 @@ loadSkills(); if (p === "session" && !fresh) loadSession(); + if (p === "research") + loadResearch(); if (p === "git") loadGit(); if (p === "dev" && !fresh) @@ -1456,4 +1458,137 @@ window.toggleSystemPrompt = toggleSystemPrompt; window.loadGit = loadGit; window.saveLogSnapshot = saveLogSnapshot; + var researchCurrentTaskId = null; + var STATUS_COLORS = { + pending: { bg: "rgba(234,179,8,0.15)", text: "#ca8a04" }, + active: { bg: "rgba(59,130,246,0.15)", text: "#2563eb" }, + completed: { bg: "rgba(34,197,94,0.15)", text: "#16a34a" }, + failed: { bg: "rgba(239,68,68,0.15)", text: "#dc2626" }, + canceled: { bg: "rgba(107,114,128,0.15)", text: "#6b7280" } + }; + async function loadResearch() { + var el = document.getElementById("research-tasks"); + var loading = document.getElementById("research-loading"); + loading.classList.remove("hidden"); + el.innerHTML = ""; + try { + var resp = await fetch(API_BASE + "/miniapp/api/research?initData=" + encodeURIComponent(initData)); + var tasks = await resp.json(); + loading.classList.add("hidden"); + if (!tasks || tasks.length === 0) { + el.innerHTML = '
No research tasks yet.
'; + return; + } + el.innerHTML = tasks.map(function(t) { + var sc = STATUS_COLORS[t.status] || STATUS_COLORS.pending; + return `
` + '
' + '' + esc(t.title) + "" + '' + t.status + "" + "
" + (t.description ? '
' + esc(t.description).substring(0, 120) + "
" : "") + '
' + t.document_count + " docs · " + new Date(t.created_at).toLocaleDateString() + "
" + "
"; + }).join(""); + } catch (e) { + loading.classList.add("hidden"); + el.innerHTML = '
Failed to load tasks.
'; + } + } + function esc(s) { + var d = document.createElement("div"); + d.textContent = s; + return d.innerHTML; + } + async function openResearchTask(id) { + researchCurrentTaskId = id; + document.getElementById("research-list-view").classList.add("hidden"); + document.getElementById("research-detail-view").classList.remove("hidden"); + var el = document.getElementById("research-detail-content"); + el.innerHTML = '
Loading...
'; + try { + var resp = await fetch(API_BASE + "/miniapp/api/research/" + id + "?initData=" + encodeURIComponent(initData)); + var task = await resp.json(); + var sc = STATUS_COLORS[task.status] || STATUS_COLORS.pending; + var canCancel = task.status === "pending" || task.status === "active"; + var canReopen = task.status === "completed" || task.status === "failed"; + var html = '
' + '
' + '' + esc(task.title) + "" + '' + task.status + "" + "
"; + if (task.description) { + html += '
' + esc(task.description) + "
"; + } + html += '
' + "Created: " + new Date(task.created_at).toLocaleString() + (task.completed_at ? " · Completed: " + new Date(task.completed_at).toLocaleString() : "") + "
"; + if (canCancel || canReopen) { + html += '
'; + if (canCancel) + html += ``; + if (canReopen) + html += ``; + html += "
"; + } + html += "
"; + html += '
Documents (' + task.documents.length + ")
"; + if (task.documents.length === 0) { + html += '
No documents yet.
'; + } else { + html += task.documents.map(function(d) { + return `
` + '
' + '#' + d.seq + "" + '' + esc(d.title) + "" + '' + d.doc_type + "" + "
" + (d.summary ? '
' + esc(d.summary) + "
" : "") + '" + "
"; + }).join(""); + } + el.innerHTML = html; + } catch (e) { + el.innerHTML = '
Failed to load task.
'; + } + } + async function toggleResearchDoc(card, taskId, docId) { + var body = card.querySelector(".research-doc-body"); + if (!body) + return; + if (!body.classList.contains("hidden")) { + body.classList.add("hidden"); + return; + } + body.classList.remove("hidden"); + if (body.dataset.loaded) + return; + try { + var resp = await fetch(API_BASE + "/miniapp/api/research/" + taskId + "/doc/" + docId + "?initData=" + encodeURIComponent(initData)); + var data = await resp.json(); + body.dataset.loaded = "1"; + body.innerHTML = '
' + esc(data.content) + "
"; + } catch (e) { + body.innerHTML = '
Failed to load document.
'; + } + } + async function researchAction(taskId, action) { + try { + await fetch(API_BASE + "/miniapp/api/research/" + taskId + "?initData=" + encodeURIComponent(initData), { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ action }) }); + openResearchTask(taskId); + } catch (e) {} + } + function showResearchList() { + document.getElementById("research-detail-view").classList.add("hidden"); + document.getElementById("research-list-view").classList.remove("hidden"); + researchCurrentTaskId = null; + loadResearch(); + } + function showNewTaskForm() { + document.getElementById("research-new-form").classList.remove("hidden"); + document.getElementById("research-title").focus(); + } + function hideNewTaskForm() { + document.getElementById("research-new-form").classList.add("hidden"); + document.getElementById("research-title").value = ""; + document.getElementById("research-desc").value = ""; + } + async function createResearchTask() { + var title = document.getElementById("research-title").value.trim(); + var desc = document.getElementById("research-desc").value.trim(); + if (!title) + return; + try { + await fetch(API_BASE + "/miniapp/api/research?initData=" + encodeURIComponent(initData), { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ title, description: desc }) }); + hideNewTaskForm(); + loadResearch(); + } catch (e) {} + } + window.showNewTaskForm = showNewTaskForm; + window.hideNewTaskForm = hideNewTaskForm; + window.createResearchTask = createResearchTask; + window.openResearchTask = openResearchTask; + window.toggleResearchDoc = toggleResearchDoc; + window.researchAction = researchAction; + window.showResearchList = showResearchList; })(); diff --git a/pkg/miniapp/static/index.html b/pkg/miniapp/static/index.html index 8a2d6b7e1..f1d950877 100644 --- a/pkg/miniapp/static/index.html +++ b/pkg/miniapp/static/index.html @@ -19,6 +19,7 @@ + @@ -79,6 +80,31 @@ +
+
+
+ Research Tasks + +
+ + +
+
+ +
+
Loading git log...