- 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.
407 lines
14 KiB
Markdown
407 lines
14 KiB
Markdown
# 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 item’s 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"
|
||
```
|