Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ CMD_DIR := ./cmd/knowledgehub
ui:
cd ui && bun install && bun run build
rm -rf $(CMD_DIR)/ui/build
mkdir -p $(CMD_DIR)/ui
cp -r ui/build $(CMD_DIR)/ui/build

build: ui
Expand Down
5 changes: 5 additions & 0 deletions internal/routes/quickadd.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,11 @@ func HandleQuickAddDirect(app core.App, body QuickAddRequest, client *http.Clien

// Trigger AI processing in background
go func() {
defer func() {
if r := recover(); r != nil {
log.Printf("AI processing panicked for quick-add entry %s: %v", entry.Id, r)
}
}()
if aiErr := ai.SummarizeAndScore(app, entry); aiErr != nil {
log.Printf("AI processing failed for quick-add entry %s: %v", entry.Id, aiErr)
entry.Set("processing_status", "failed")
Expand Down
2 changes: 2 additions & 0 deletions openspec/changes/auto-tagging/.openspec.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-03-11
3 changes: 3 additions & 0 deletions openspec/changes/auto-tagging/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# auto-tagging

AI-powered auto-tagging of articles during processing
33 changes: 33 additions & 0 deletions openspec/changes/auto-tagging/proposal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
## Why

Articles currently have no categorization beyond which source they come from. When browsing the feed, there is no way to find all articles about a topic (e.g., "distributed systems" or "Go") across different sources. The AI processing pipeline already reads each article to generate summaries and star ratings — adding tag extraction to the same LLM call is low marginal cost and enables powerful topic-based filtering.

## What Changes

- Add a `tags` JSON field to the entries collection, storing an array of lowercase tag strings
- Extend the existing AI processing prompt to also extract 1-5 topic tags per article alongside the summary and star rating (single LLM call, no extra API cost)
- Add a tag filter to the feed view UI that allows multi-select filtering by tag
- Tags are displayed on entry cards as small chips
- Popular tags are surfaced in the filter UI, ordered by frequency

## Capabilities

### New Capabilities
- `tag-filtering`: Multi-select tag filter on the feed view. Shows the most frequently used tags. Selecting one or more tags filters entries to those containing at least one of the selected tags. Composes with all other filters.

### Modified Capabilities
- `ai-processing`: Extend the summarization+scoring prompt to also return a `tags` array of 1-5 lowercase topic strings. Tags capture the article's main subjects (e.g., "go", "distributed-systems", "performance", "llm"). Tags are stored in the entry's `tags` JSON field.
- `feed-view`: Display tags as small chips on entry cards. Add a tag filter section (collapsible, like the current source filter) showing popular tags as selectable pills.

## Impact

- **Backend**: Modify the AI processing prompt in the Go engine to request tags in the LLM response JSON. Parse and store tags in the new field. Add a one-time backfill endpoint or CLI command to tag existing untagged entries.
- **Frontend**: New tag chips on `EntryCard.svelte`. New tag filter section on `+page.svelte`. PocketBase filter using `tags ~ '"tagname"'` for JSON array contains.
- **Database**: Add `tags` JSON field to the entries collection (nullable, defaults to null for existing entries).

## Non-goals

- User-defined custom tags or manual tagging (AI-only for now)
- Tag taxonomy or hierarchy (flat list is sufficient)
- Tag management UI (tags are derived, not curated)
- Reprocessing all existing articles automatically on deploy (backfill is opt-in via CLI)
29 changes: 29 additions & 0 deletions openspec/changes/auto-tagging/specs/ai-processing/spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
## MODIFIED Requirements

### Requirement: Combined summarization, scoring, and tagging (MODIFIED)
The system SHALL perform summarization, scoring, optional takeaway extraction, and tag extraction in a single LLM call per entry. The LLM SHALL return a `tags` array of 1-5 lowercase topic strings that capture the article's main subjects. Tags SHALL use lowercase kebab-case (e.g., "distributed-systems", "go", "performance"). When an article covers no clear topics, the tags array MAY be empty.

#### Scenario: Single prompt produces summary, score, and tags
- **WHEN** a new entry about Go concurrency patterns is processed
- **THEN** one OpenRouter API call returns the summary, star rating, and tags such as ["go", "concurrency", "goroutines"]

#### Scenario: Article with broad topics
- **WHEN** a new entry is a general industry news roundup
- **THEN** the tags array contains high-level topics like ["industry-news", "tech"] rather than enumerating every mentioned subject

#### Scenario: LLM returns invalid tags
- **WHEN** the LLM returns tags that are not lowercase or contain spaces
- **THEN** the system normalizes them to lowercase kebab-case before storing

## ADDED Requirements

### Requirement: Backfill tags for existing entries
The system SHALL provide a CLI command or API endpoint to backfill tags for existing entries that have a summary but no tags. The backfill SHALL reuse the same LLM prompt, sending the existing summary and title to extract tags without regenerating the summary or score.

#### Scenario: Backfill untagged entries
- **WHEN** the backfill command is run and 200 entries have null tags
- **THEN** the system processes each entry through the LLM to extract tags, storing the result in the tags field

#### Scenario: Backfill skips already-tagged entries
- **WHEN** the backfill command is run and an entry already has a non-null tags array
- **THEN** that entry is skipped
31 changes: 31 additions & 0 deletions openspec/changes/auto-tagging/specs/feed-view/spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
## ADDED Requirements

### Requirement: Display tags on entry cards
The system SHALL display an entry's tags as small chips on the entry card, below the summary/takeaways area. Tags are rendered in a compact, muted style that does not dominate the card layout. Entries with null or empty tags show no tag section.

#### Scenario: Entry with tags
- **WHEN** an entry has tags ["go", "concurrency", "performance"]
- **THEN** three small tag chips are displayed on the card

#### Scenario: Entry without tags
- **WHEN** an entry has null or empty tags
- **THEN** no tag section is rendered on the card

### Requirement: Filter by tags
The system SHALL provide a tag filter section on the feed view (collapsible, similar to the resource filter) showing the most frequently used tags across visible entries. The user can select one or more tags. When tags are selected, only entries containing at least one of the selected tags are shown (OR logic). The tag filter composes with all other active filters.

#### Scenario: Select a single tag
- **WHEN** user selects the "go" tag from the tag filter
- **THEN** only entries that have "go" in their tags array are shown

#### Scenario: Select multiple tags
- **WHEN** user selects "go" and "rust" from the tag filter
- **THEN** entries that have "go" OR "rust" (or both) in their tags are shown

#### Scenario: Tag frequency ordering
- **WHEN** the tag filter section is expanded
- **THEN** tags are ordered by frequency (most common first), showing up to 20 tags

#### Scenario: Tag filter combined with search
- **WHEN** user has a search query "memory" and selects tag "performance"
- **THEN** only entries matching "memory" in title/summary/resource AND having "performance" in tags are shown
40 changes: 40 additions & 0 deletions openspec/changes/auto-tagging/tasks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
## 1. Database Schema

- [ ] 1.1 Add `tags` JSON field (nullable) to the entries collection in `collections.go`
- [ ] 1.2 Add migration logic to update existing collection schema (PocketBase auto-migration on startup)

## 2. AI Processing Pipeline

- [ ] 2.1 Update the LLM prompt in the processing engine to request a `tags` array (1-5 lowercase kebab-case topic strings) alongside summary, stars, and takeaways
- [ ] 2.2 Parse the `tags` field from the LLM JSON response and normalize values (lowercase, replace spaces with hyphens)
- [ ] 2.3 Store parsed tags in the entry's `tags` field during processing
- [ ] 2.4 Add test: verify LLM response parsing handles tags field correctly (present, absent, malformed)

## 3. Backfill Command

- [ ] 3.1 Create a backfill endpoint or CLI command that queries entries where `tags` is null and `summary` is not null
- [ ] 3.2 For each untagged entry, send title + summary to the LLM with a tag-extraction-only prompt (no need to regenerate summary/stars)
- [ ] 3.3 Store extracted tags, skip entries that already have tags
- [ ] 3.4 Add rate limiting / batch size control to avoid overwhelming OpenRouter

## 4. Frontend: Tag Display on Entry Cards

- [ ] 4.1 Add tag chip rendering to `EntryCard.svelte` — small, muted pills below the summary/takeaways section
- [ ] 4.2 Only render the tag section when `entry.tags` is a non-empty array
- [ ] 4.3 Style tag chips to be compact and visually secondary to the card content

## 5. Frontend: Tag Filter

- [ ] 5.1 Add a `selectedTags` state variable (string array) and a `tagFilter` collapsible section on `+page.svelte`, similar to the resource filter
- [ ] 5.2 Compute tag frequencies from currently loaded entries and display the top 20 tags ordered by frequency
- [ ] 5.3 Tags are multi-select pills — clicking toggles selection
- [ ] 5.4 Wire tag filter into entry filtering: when tags are selected, filter entries to those containing at least one selected tag (OR logic). This can be client-side since tags are already loaded with entries.
- [ ] 5.5 Tag filter composes with search, star, read status, and resource filters

## 6. Testing & Verification

- [ ] 6.1 Run Go test suite (`go test ./internal/... -count=1`) and verify all pass
- [ ] 6.2 Manual test: process a new article, verify tags appear on the entry card
- [ ] 6.3 Manual test: run backfill on existing entries, verify tags are populated
- [ ] 6.4 Manual test: select tags in the filter, verify entries filter correctly
- [ ] 6.5 Manual test: combine tag filter with search and star filter, verify composition
2 changes: 2 additions & 0 deletions openspec/changes/enhanced-search/.openspec.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-03-11
3 changes: 3 additions & 0 deletions openspec/changes/enhanced-search/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# enhanced-search

Full-text search and multi-source filtering for the articles feed
31 changes: 31 additions & 0 deletions openspec/changes/enhanced-search/proposal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
## Why

Finding a specific article in the feed is difficult. There is no text search, so the only way to narrow results is by toggling read status, star rating, or selecting a single source from a collapsible pill list. If you remember an article was by "Yegge" but don't remember which source it came from, you have to manually scan through entries or click through sources one by one. As the article collection grows, this becomes increasingly painful.

## What Changes

- Add a search bar at the top of the feed view that filters entries by matching against title, summary, and resource name
- The search is server-side using PocketBase filter operators, so it works within the existing 200-item page and composes with all other active filters
- The resource filter panel switches from single-select to multi-select, so users can view articles from several sources at once
- Active filters (search query, selected sources) are shown as dismissible chips for clarity

## Capabilities

### New Capabilities
- `article-search`: Full-text search bar on the feed view that filters entries by title, summary, and expanded resource name. Composes with existing read status, star, and resource filters.

### Modified Capabilities
- `feed-view`: Add the search bar above existing filter controls. Change resource filter from single-select to multi-select with dismissible chips. Add empty-state messaging when search returns no results.

## Impact

- **Frontend**: New search input component on `+page.svelte`. Modified resource filter from radio-style to checkbox-style pills. Dismissible filter chip display. The `loadEntries` function gains a search term parameter that adds PocketBase `~` filter clauses.
- **Backend**: No backend changes required. PocketBase already supports the `~` (LIKE) operator and relation field filtering needed for this feature.
- **Database**: No schema changes. Searching uses existing `title`, `summary` fields and the expanded `resource.name` field.

## Non-goals

- Dedicated search results page or search history
- Fuzzy/typo-tolerant search (PocketBase `~` is substring match, which is sufficient)
- Pagination beyond the current 200-item limit (separate concern)
- Search within article body/raw_content (too heavy, summary is sufficient)
48 changes: 48 additions & 0 deletions openspec/changes/enhanced-search/specs/feed-view/spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
## ADDED Requirements

### Requirement: Search entries by text
The system SHALL provide a search bar on the feed view that filters entries by matching the search query against title, summary, and resource name. The search SHALL be case-insensitive and match substrings. The search SHALL compose with all other active filters (read status, stars, resource). When the search field is cleared, the full (filtered) entry list SHALL be restored.

#### Scenario: Search by article title
- **WHEN** user types "CRDT" into the search bar with readFilter set to "all"
- **THEN** only entries whose title, summary, or resource name contains "CRDT" (case-insensitive) are displayed

#### Scenario: Search by resource name
- **WHEN** user types "Yegge" into the search bar
- **THEN** entries from resources whose name contains "Yegge" are displayed, regardless of the entry's title or summary content

#### Scenario: Search combined with star filter
- **WHEN** user types "Go" into the search bar and star filter is set to 4+
- **THEN** only entries matching "Go" with effective stars >= 4 are displayed

#### Scenario: Empty search results
- **WHEN** user types a query that matches no entries
- **THEN** the view shows an empty state message indicating no entries match the search

#### Scenario: Clear search
- **WHEN** user clears the search bar (empty string)
- **THEN** the search filter is removed and all entries matching other active filters are shown

### Requirement: Debounced search
The system SHALL debounce search input by 300ms to avoid excessive API calls while typing.

#### Scenario: Rapid typing
- **WHEN** user types "dist" quickly (each character within 300ms)
- **THEN** only one API call is made after typing stops, filtering for "dist"

## MODIFIED Requirements

### Requirement: Filter by resource (MODIFIED)
The system SHALL provide a resource filter that allows selecting multiple resources simultaneously. When one or more resources are selected, only entries from those resources are shown. The selected resources SHALL be displayed as dismissible chips. Deselecting all resources restores the "all sources" view.

#### Scenario: Select multiple resources
- **WHEN** user selects "Go Blog" and "Hacker News" from the resource filter
- **THEN** only entries from those two sources are displayed

#### Scenario: Dismiss a resource chip
- **WHEN** user clicks the dismiss button on the "Go Blog" chip while "Go Blog" and "Hacker News" are selected
- **THEN** "Go Blog" is deselected and only "Hacker News" entries are shown

#### Scenario: Clear all resource filters
- **WHEN** user deselects all resource chips or clicks "All"
- **THEN** entries from all active resources are shown
28 changes: 28 additions & 0 deletions openspec/changes/enhanced-search/tasks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
## 1. Search Bar Component

- [ ] 1.1 Add a search input field to `+page.svelte` above the existing filter controls, with placeholder text "Search articles..."
- [ ] 1.2 Add `searchQuery` state variable and a 300ms debounce mechanism (use a `setTimeout`-based debounce or a reactive `$effect` with delay)
- [ ] 1.3 Wire the debounced search query into `loadEntries()` — when non-empty, add PocketBase filter clauses: `(title ~ '{query}' || summary ~ '{query}' || resource.name ~ '{query}')`
- [ ] 1.4 Trigger `loadEntries()` reactively when `searchQuery` changes (add to the existing `$effect` alongside `readFilter` and `resourceFilter`)

## 2. Multi-Select Resource Filter

- [ ] 2.1 Change `resourceFilter` state from `string` (single ID) to `string[]` (array of IDs)
- [ ] 2.2 Update resource pill buttons to toggle selection (add/remove from array) instead of single-select
- [ ] 2.3 Update `loadEntries()` filter construction — when multiple resources selected, build an OR filter: `(resource = 'id1' || resource = 'id2' || ...)`
- [ ] 2.4 Add dismissible chip display above the entry list showing selected resource names with an X button to deselect each
- [ ] 2.5 Update the "All" button to clear the array, and the collapsible header to show count of selected sources

## 3. Empty State & UX

- [ ] 3.1 Add a search-specific empty state message: "No entries match your search" when search is active and results are empty
- [ ] 3.2 Add a clear-search affordance (X button inside the search input or a "Clear filters" link)

## 4. Testing & Verification

- [ ] 4.1 Manual test: type a search query, verify entries filter by title match
- [ ] 4.2 Manual test: search for a resource name (e.g., "Yegge"), verify entries from that source appear
- [ ] 4.3 Manual test: combine search with star filter and read filter, verify all compose correctly
- [ ] 4.4 Manual test: select multiple resources, verify only entries from those sources appear
- [ ] 4.5 Manual test: dismiss a resource chip, verify filter updates
- [ ] 4.6 Manual test: verify debounce — rapid typing should not cause excessive loading flicker
Loading