zotero-kb/docs/superpowers/plans/2026-04-16-project-items-and-inline-card-detail.md
Saberlve cf7e2feb19 feat: Implement project management features including rename and delete functionality
- Added project renaming capability in the workspace with appropriate API endpoints.
- Implemented project deletion functionality, ensuring project directories are removed.
- Updated UI to support project renaming and deletion, including inline editing and confirmation dialogs.
- Enhanced batch generation feature for project items with selection and progress tracking.
- Added tests for project renaming and deletion to ensure functionality and integrity.
2026-04-21 16:55:42 +08:00

407 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Project Items And Inline Card Detail Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Show imported project items immediately after import, improve collection row readability, and render item detail inline under the active entry instead of in a fixed detail area.
**Architecture:** Normalize project view data around a single `items` list in `ProjectService`, with each item carrying `card_status` plus card fields when available. Update the center panel renderer in the inline template script to render both pending and done items, and expand one items detail inline at a time.
**Tech Stack:** Python, FastAPI template rendering, inline HTML/CSS/JavaScript, pytest
---
### Task 1: Define Pending-Item Project View Contract
**Files:**
- Modify: `tests/test_projects.py`
- Modify: `tests/test_api.py`
- Test: `tests/test_projects.py`
- Test: `tests/test_api.py`
- [ ] **Step 1: Write the failing test**
Add to `tests/test_projects.py`:
```python
def test_add_item_to_project_returns_pending_item_when_card_not_generated(tmp_path: Path) -> None:
config = AppConfig(workspace_dir=tmp_path / "workspace", zotero_data_dir=tmp_path / "zotero")
workspace = Workspace(config)
workspace.create_project("thesis-ch2", "Thesis Chapter 2", "openai", "gpt-5-mini")
items_index_path = config.workspace_dir / "library" / "index" / "items.json"
items_index_path.parent.mkdir(parents=True, exist_ok=True)
items_index_path.write_text(
json.dumps(
{
"PAPER0001": {
"item_key": "PAPER0001",
"title": "Pending Item",
"creators": ["Alice Smith"],
"year": "2024",
"item_type": "journalArticle",
}
},
ensure_ascii=False,
),
encoding="utf-8",
)
service = ProjectService(config.workspace_dir)
payload = service.add_items("thesis-ch2", ["PAPER0001"])
assert payload["selected_items"] == ["PAPER0001"]
assert len(payload["items"]) == 1
assert payload["items"][0]["item_key"] == "PAPER0001"
assert payload["items"][0]["card_status"] == "pending"
```
Add to `tests/test_api.py`:
```python
def test_import_item_keys_endpoint_returns_pending_item_in_project_view(tmp_path: Path) -> None:
config = make_test_config(tmp_path)
config.bridge_file.unlink()
app = create_app(config, llm_client=FakeLlmClient())
create_project = _route(app, "/api/projects", "POST")
import_keys = _route(app, "/api/projects/{project_id}/imports/item-keys", "POST")
create_project(
CreateProjectRequest(
project_id="thesis-ch2",
name="Thesis Chapter 2",
llm_provider="openai",
llm_model="gpt-5-mini",
)
)
payload = import_keys("thesis-ch2", {"item_keys": ["PAPER0001"]})
assert payload["project_view"]["selected_items"] == ["PAPER0001"]
assert payload["project_view"]["items"][0]["item_key"] == "PAPER0001"
assert payload["project_view"]["items"][0]["card_status"] == "pending"
```
- [ ] **Step 2: Run test to verify it fails**
Run: `UV_CACHE_DIR=/tmp/uv-cache uv run pytest tests/test_projects.py tests/test_api.py -q`
Expected: FAIL because project view currently only returns `cards`, not normalized `items`.
- [ ] **Step 3: Write minimal implementation**
Modify `src/zotero_kb/projects.py` so `_rebuild_project_index()` returns normalized items for both pending and done entries:
```python
def _rebuild_project_index(self, project_id: str, selected_items: list[str]) -> dict[str, object]:
cards_index = self._read_json(self.index_dir / "cards.json")
items_index = self._read_json(self.index_dir / "items.json")
collections_index = self._read_json(self.index_dir / "collections.json")
project_items: list[dict[str, object]] = []
project_cards: list[dict[str, object]] = []
for item_key in selected_items:
item_data = items_index.get(item_key, {})
card_data = cards_index.get(item_key)
item_payload = {
"item_key": item_key,
"title": item_data.get("title", "Untitled"),
"creators": item_data.get("creators", []),
"year": item_data.get("year"),
"item_type": item_data.get("item_type", "unknown"),
"card_status": "done" if card_data else "pending",
"summary": card_data.get("summary") if card_data else None,
"claims": card_data.get("claims", []) if card_data else [],
"quotable_spans": card_data.get("quotable_spans", []) if card_data else [],
}
project_items.append(item_payload)
if card_data:
project_cards.append(card_data)
project_collections = [
payload
for payload in collections_index.values()
if set(payload.get("item_keys", [])) & set(selected_items)
]
payload = {
"project_id": project_id,
"selected_items": selected_items,
"items": project_items,
"cards": project_cards,
"collections": project_collections,
}
```
- [ ] **Step 4: Run test to verify it passes**
Run: `UV_CACHE_DIR=/tmp/uv-cache uv run pytest tests/test_projects.py tests/test_api.py -q`
Expected: PASS
- [ ] **Step 5: Commit**
```bash
git add tests/test_projects.py tests/test_api.py src/zotero_kb/projects.py
git commit -m "feat: add pending project items to project view"
```
### Task 2: Define UI Contract For Inline Project Item Detail
**Files:**
- Modify: `tests/test_ui.py`
- Test: `tests/test_ui.py`
- [ ] **Step 1: Write the failing test**
Add to `tests/test_ui.py`:
```python
def test_index_has_project_item_list_and_inline_detail_hooks(tmp_path) -> None:
html = _get_index_html(tmp_path)
assert 'id="project-item-list"' in html
assert "expandedProjectItemKey" in html
assert "function renderProjectItems(items)" in html
assert "function toggleProjectItemDetail(itemKey)" in html
assert 'class="project-item-detail"' in html
```
```python
def test_index_collection_rows_use_readable_unselected_text(tmp_path) -> None:
html = _get_index_html(tmp_path)
assert ".project-item strong" in html
assert ".project-item .meta" in html
assert ".collection-row-toggle" in html
```
- [ ] **Step 2: Run test to verify it fails**
Run: `UV_CACHE_DIR=/tmp/uv-cache uv run pytest tests/test_ui.py -q`
Expected: FAIL because the page still renders only cards and uses the fixed detail region.
- [ ] **Step 3: Write minimal implementation**
Update `src/zotero_kb/templates/index.html`:
- replace the center list container with:
```html
<div id="project-item-list" class="card-list"></div>
```
- replace the fixed detail surface with a passive note:
```html
<div class="section surface">
<h3>项目状态</h3>
<div class="empty">导入后,项目条目会立即出现在上方列表;点击条目可就地展开详情。</div>
</div>
```
- add state:
```javascript
expandedProjectItemKey: null,
```
- add renderers:
```javascript
function toggleProjectItemDetail(itemKey) {
state.expandedProjectItemKey = state.expandedProjectItemKey === itemKey ? null : itemKey;
renderProjectItems(state.currentProjectItems);
}
```
```javascript
function renderProjectItems(items) {
state.currentProjectItems = items || [];
if (!state.currentProjectItems.length) {
elements.projectItemList.innerHTML = '<div class="empty">当前项目还没有条目。先导入 Zotero 文献。</div>';
return;
}
elements.projectItemList.innerHTML = "";
for (const item of state.currentProjectItems) {
const card = document.createElement("div");
card.className = "card-item";
const statusLabel = item.card_status === "done" ? "已生成卡片" : "未生成卡片";
card.innerHTML = `
<strong>${escapeHtml(item.title)}</strong>
<div class="meta">${item.item_key} · ${item.year || "-"} · ${item.item_type || "-"}</div>
<p>${item.card_status === "done" ? (item.summary || "暂无摘要") : "已导入项目,尚未生成卡片。"}</p>
<div class="row">
<button type="button" class="secondary">查看详情</button>
<button type="button" class="${item.card_status === "done" ? "secondary" : ""}">${statusLabel}</button>
<button type="button" class="danger">移出项目</button>
</div>
`;
const [detailButton, statusButton, removeButton] = card.querySelectorAll("button");
detailButton.addEventListener("click", () => toggleProjectItemDetail(item.item_key));
statusButton.disabled = true;
removeButton.addEventListener("click", () => removeCard(item.item_key));
if (state.expandedProjectItemKey === item.item_key) {
const detail = document.createElement("div");
detail.className = "project-item-detail";
detail.innerHTML = item.card_status === "done"
? `
<div class="meta">${item.item_key}</div>
<p>${item.summary || "暂无摘要"}</p>
<h4>Claims</h4>
<ul>${(item.claims || []).map((claim) => `<li>${escapeHtml(claim)}</li>`).join("") || "<li>暂无 claims</li>"}</ul>
<h4>Quotable</h4>
<ul>${(item.quotable_spans || []).map((quote) => `<li>${escapeHtml(quote)}</li>`).join("") || "<li>暂无可引用片段</li>"}</ul>
`
: `
<div class="meta">${item.item_key} · ${item.year || "-"} · ${item.item_type || "-"}</div>
<p>这篇文献已经导入到当前项目,但还没有生成卡片内容。</p>
`;
card.appendChild(detail);
}
elements.projectItemList.appendChild(card);
}
}
```
- add CSS:
```css
.project-item strong,
.project-item .meta {
color: var(--ink);
}
.project-item.active strong,
.project-item.active .meta {
color: #f5f7ef;
}
.collection-row-toggle {
color: var(--accent-strong);
font-weight: 700;
}
.project-item-detail {
margin-top: 0.85rem;
padding-top: 0.85rem;
border-top: 1px solid var(--line);
}
```
- [ ] **Step 4: Run test to verify it passes**
Run: `UV_CACHE_DIR=/tmp/uv-cache uv run pytest tests/test_ui.py -q`
Expected: PASS
- [ ] **Step 5: Commit**
```bash
git add tests/test_ui.py src/zotero_kb/templates/index.html
git commit -m "feat: render project items with inline detail"
```
### Task 3: Wire Project Item Rendering Into Import And Project Selection Flows
**Files:**
- Modify: `src/zotero_kb/templates/index.html`
- Test: `tests/test_api.py`
- Test: `tests/test_ui.py`
- [ ] **Step 1: Write the failing test**
Reuse the UI/API failures from Tasks 1 and 2. No new production code first.
- [ ] **Step 2: Run tests to verify the current behavior fails**
Run: `UV_CACHE_DIR=/tmp/uv-cache uv run pytest tests/test_projects.py tests/test_api.py tests/test_ui.py -q`
Expected: FAIL until the project list renderer uses `items`.
- [ ] **Step 3: Write minimal implementation**
Update the inline script in `src/zotero_kb/templates/index.html`:
- state:
```javascript
currentProjectItems: [],
```
- elements:
```javascript
projectItemList: document.getElementById("project-item-list"),
```
- remove `showCardDetail()` and stop using `elements.cardDetail`
- change empty-state and load paths:
```javascript
async function selectProject(projectId, rerender = true) {
state.currentProjectId = projectId;
updateImportActionState();
if (rerender) {
renderProjects();
}
const payload = await api(`/api/projects/${projectId}/cards`);
renderProjectItems(payload.items || []);
}
```
```javascript
renderProjectItems(payload.project_view?.items || []);
```
```javascript
renderProjectItems([]);
```
- [ ] **Step 4: Run full targeted tests**
Run: `UV_CACHE_DIR=/tmp/uv-cache uv run pytest tests/test_projects.py tests/test_api.py tests/test_ui.py -q`
Expected: PASS
- [ ] **Step 5: Commit**
```bash
git add src/zotero_kb/templates/index.html tests/test_projects.py tests/test_api.py tests/test_ui.py
git commit -m "feat: show imported project items before card generation"
```
### Task 4: Final Verification
**Files:**
- Modify: none
- Test: `tests/test_projects.py`
- Test: `tests/test_api.py`
- Test: `tests/test_ui.py`
- [ ] **Step 1: Run full test suite**
Run: `UV_CACHE_DIR=/tmp/uv-cache uv run pytest -q`
Expected: PASS
- [ ] **Step 2: Run JS syntax guard**
Run: `UV_CACHE_DIR=/tmp/uv-cache uv run pytest tests/test_ui.py::test_index_inline_script_is_valid_javascript -q`
Expected: PASS
- [ ] **Step 3: Manual verification**
Run:
```bash
UV_CACHE_DIR=/tmp/uv-cache uv run python main.py
```
Manual checks:
- unselected collections remain readable in the import window
- importing an item into a project makes it appear immediately in the center panel
- pending item detail expands inline below the clicked item
- generated item detail expands inline below the clicked item
- [ ] **Step 4: Commit**
```bash
git add src/zotero_kb/projects.py src/zotero_kb/templates/index.html tests/test_projects.py tests/test_api.py tests/test_ui.py docs/superpowers/specs/2026-04-16-project-items-and-inline-card-detail-design.md docs/superpowers/plans/2026-04-16-project-items-and-inline-card-detail.md
git commit -m "feat: show project items inline before card generation"
```