Skip to content

feat: Analytics dashboard, compression improvements, and image search#35

Merged
VedankPurohit merged 6 commits into
mainfrom
feat/compression-improvements
Feb 15, 2026
Merged

feat: Analytics dashboard, compression improvements, and image search#35
VedankPurohit merged 6 commits into
mainfrom
feat/compression-improvements

Conversation

@VedankPurohit

@VedankPurohit VedankPurohit commented Feb 15, 2026

Copy link
Copy Markdown
Owner

Summary

This branch adds several major features and improvements to LiveRecall:

Analytics Dashboard

  • New /analytics page with activity insights — daily/weekly/monthly screenshot counts, peak hours heatmap, incognito usage stats, storage breakdown, and streak tracking
  • Full backend API at /api/v1/analytics with endpoints for activity stats, daily trends, hourly heatmaps, streaks, storage info, and top OCR keywords
  • Interleaved incognito gaps in timeline view so users see when recording was paused

Compression Feature Improvements

  • Updated compression defaults: enabled=False (opt-in), after_days=60, quality=75
  • Added force recompress functionality with offset-based batching
  • Compression suggestion popup component that shows when compressible screenshots exist
  • Settings page controls for compression configuration (quality slider, age threshold, enable/disable)
  • Comprehensive test coverage for compression logic

Image-Based Search (Find Similar + Upload)

  • Find Similar: Click any screenshot in the lightbox to find visually similar ones using stored CLIP embeddings
  • Image Upload: Camera icon in search bar + drag-and-drop to search by an external image (base64 encoded)
  • All image search goes through the existing POST /search endpoint — added an optional image field (accepts int screenshot ID or string base64 data)
  • Search filters (date range, visibility, safe mode) work reactively with image search
  • No new endpoints — reuses the existing hybrid search infrastructure

Bug Fixes

  • Fixed tooltip mismatch in compression settings
  • Added proper response models to API endpoints
  • Fixed code review findings across analytics and compression features
  • Increased sqlite-vec KNN k multiplier from 2x to 10x for better filtered search results

Test plan

  • uv run pytest — all 179 tests pass
  • cd web && npm run build — no TypeScript errors
  • Manual: Open analytics page, verify charts and stats render
  • Manual: Click "Find Similar" on a screenshot, verify similar results appear
  • Manual: Click camera icon in search bar, upload an image, verify results
  • Manual: Drag an image onto the search view, verify results
  • Manual: Change date range/visibility/safe mode during image search, verify re-search
  • Manual: Check compression suggestion popup appears when compressible screenshots exist
  • Manual: Enable compression from popup or settings, verify it runs

Summary by CodeRabbit

  • New Features

    • Image-based search: find similar screenshots by dragging or uploading an image.
    • Force recompress: new Settings option to recompress older screenshots with preview and confirmation.
    • Compression suggestions: banner recommending compression improvements when eligible items exist.
    • Find Similar: quick search button in lightbox to locate similar images.
  • Chores

    • Default JPEG compression quality adjusted to 75 for improved file sizes.

Change default compression quality from 85 to 75 and age threshold
from 60 to 90 days. Add force recompression feature that allows
re-compressing all screenshots (including already compressed) older
than a user-selected age, with preview and warning UI in Settings.

- Update CompressionSettings defaults (quality 75, after_days 90)
- Add force recompression DB methods with breakdown counts
- Add batched force recompress with progress callbacks
- Add API preview and execute endpoints with Pydantic schemas
- Add force recompress modal in Settings with age selector and warnings
The previous approach fetched the same rows each batch since
get_force_recompressible_screenshots doesn't filter on is_compressed.
The processed_ids set filtered them all out, breaking the loop after
the first batch. Switch to LIMIT/OFFSET pagination instead.
- Add floating banner on timeline when compressible screenshots exist
- Banner offers one-click auto-compression enable with dismiss option
- Uses localStorage to remember dismissal across sessions
- Change default compression threshold from 90 to 60 days (2 months)
- Add confirmation checkbox to force recompress modal for safety
- Check start_force_recompress return value to handle TOCTOU race
- Save localStorage dismissal immediately, not after animation delay
- Reset confirmation checkbox when closing force recompress modal
- Handle partial failure: dismiss popup even if startCompression fails
  after config is successfully enabled
Update compression quality default from 85 to 75 in test assertions
and add 18 new tests covering force recompress service, DB methods,
and API endpoints.
Add support for searching by image (screenshot ID or base64 data) through
the existing POST /search endpoint. No new endpoints needed — the image
field on SearchRequest accepts either an int (screenshot ID to use stored
CLIP embedding) or a string (base64-encoded image to generate embedding).

Frontend changes:
- Find Similar button in lightbox sets imageSearchImage state
- Camera icon + drag-and-drop reads files as base64
- Search effect handles image search reactively — changing time range,
  visibility, or safe mode re-runs the image search automatically
- Thumbnail preview chip in search bar with clear button
- Search mode dropdown shows "Image Only" and is disabled during image search

Backend changes:
- SearchRequest.image field accepts int | str | None
- When int: fetches stored CLIP embedding via db.get_embedding()
- When str: decodes base64 and generates embedding via get_image_embedding_from_bytes()
- Source screenshot filtered from results when searching by ID
- Text embedding generation skipped when image is provided
- Added get_embedding() method to Database class
- Added get_image_embedding_from_bytes() to core/embeddings.py
- Increased KNN k multiplier from 2x to 10x for better filtered search coverage
@coderabbitai

coderabbitai Bot commented Feb 15, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

This PR introduces force recompression capability to re-compress already-compressed screenshots, adds image-based search functionality for finding similar items via drag-and-drop or file upload, and lowers default JPEG quality from 85 to 75. It includes new API endpoints, database methods, embeddings functions, UI components, and comprehensive test coverage.

Changes

Cohort / File(s) Summary
Force Recompression Backend
core/compression.py, api/routes/compression.py, core/database.py
Added start_force_recompress method and force parameter to _compress_screenshot; implemented preview and start endpoints; added get_force_recompressible_* database queries for identifying and counting eligible screenshots.
Force Recompression Models & Tests
api/schemas.py, api/tests/test_api.py, core/tests/test_compression.py
Introduced ForceRecompressPreviewRequest/Response and ForceRecompressRequest/Response schemas; added comprehensive test suite covering preview, confirmation validation, already-running detection, and pagination.
Image-based Search
api/routes/search.py, core/embeddings.py, core/database.py
Enhanced search endpoint to accept optional image parameter (screenshot ID or base64); added get_image_embedding_from_bytes function; added get_embedding database method for retrieving stored embeddings.
Image Search Schema & Tests
api/schemas.py, api/tests/test_api.py
Modified SearchRequest to make query optional and added image field; added test coverage for image-based search via ID, base64 encoding, error handling, and source filtering.
Force Recompression UI
web/src/app/settings/page.tsx, web/src/types/index.ts
Added Force Recompress modal in Settings with age selection, preview statistics, confirmation, and integration with new API endpoints; introduced ForceRecompressPreview type.
Image Search Frontend
web/src/app/page.tsx, web/src/lib/api.ts
Implemented image search flow with drag-and-drop upload, image preview chip, thumbnail display; updated search API to accept image parameter; added search triggering on image selection.
Compression Suggestion Component
web/src/components/CompressionSuggestion.tsx
New dismissible banner component fetching compression stats, calculating estimated savings, and offering one-click auto-compression enablement.
Configuration & Dependencies
core/config.py, core/tests/conftest.py, core/tests/test_config.py, pyproject.toml
Lowered default JPEG quality from 85 to 75; removed obsolete dependencies (opencv-python, scikit-image, mss, sqlite-vec, transformers, pytesseract, pydantic, pystray, httpx); added python-multipart.
Database Tests
core/tests/test_database.py
Added test_get_embedding and test_get_embedding_not_found to verify embedding retrieval and non-existence handling.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant WebUI as Web UI
    participant API as API Server
    participant DB as Database
    participant Compression as Compression Service

    User->>WebUI: Click "Force Recompress"
    WebUI->>API: POST /force-recompress/preview<br/>(older_than_days)
    API->>DB: get_force_recompressible_count()
    DB-->>API: {total, already_compressed, not_compressed}
    API-->>WebUI: Preview response with stats
    WebUI->>User: Show modal with counts & warning
    User->>WebUI: Confirm & click "Recompress"
    WebUI->>API: POST /force-recompress<br/>(older_than_days, confirm=true)
    API->>Compression: start_force_recompress()
    Compression->>DB: get_force_recompressible_count()
    DB-->>Compression: Total work count
    loop For each batch
        Compression->>DB: get_force_recompressible_screenshots(limit, offset)
        DB-->>Compression: Batch of screenshots
        Compression->>Compression: _compress_screenshot(force=true)
        Compression->>DB: Mark as compressed
    end
    Compression-->>API: Process complete
    API-->>WebUI: Success response
    WebUI->>User: Show completion status
Loading
sequenceDiagram
    actor User
    participant WebUI as Web UI
    participant API as API Server
    participant Embeddings as Embeddings Service
    participant DB as Database

    User->>WebUI: Drag/drop image or click upload
    WebUI->>WebUI: Generate preview & encode base64
    WebUI->>API: POST /search<br/>(image=base64_or_id, query='')
    API->>API: Validate image parameter
    alt image is ID
        API->>DB: get_embedding(screenshot_id)
        DB-->>API: Stored embedding vector
    else image is base64
        API->>Embeddings: get_image_embedding_from_bytes()
        Embeddings-->>API: Computed embedding vector
    end
    API->>DB: Search with embedding vector
    DB-->>API: Similar results (including source)
    API->>API: Filter out source image
    API-->>WebUI: Search results
    WebUI->>User: Display similar screenshots
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~65 minutes

Possibly related PRs

  • PR #34: Implements identical force-recompress functionality with overlapping endpoint definitions, request/response schemas, and compression service methods.
  • PR #28: Modifies api/routes/search.py and search request handling similarly, introducing parameter changes to the shared search endpoint.

Poem

🐰 Hop, skip, and re-compress!
Force those old screenshots to their best,
Drag images in, find twins with glee,
Quality slides to seventy-five, 🖼️
Our fuzzy friend's search springs to life!

🚥 Pre-merge checks | ✅ 2 | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Merge Conflict Detection ⚠️ Warning ❌ Merge conflicts detected (10 files):

⚔️ api/routes/search.py (content)
⚔️ api/schemas.py (content)
⚔️ api/tests/test_api.py (content)
⚔️ core/database.py (content)
⚔️ core/embeddings.py (content)
⚔️ core/tests/test_database.py (content)
⚔️ pyproject.toml (content)
⚔️ uv.lock (content)
⚔️ web/src/app/page.tsx (content)
⚔️ web/src/lib/api.ts (content)

These conflicts must be resolved before merging into main.
Resolve conflicts locally and push changes to this branch.
Title check ❓ Inconclusive The PR title mentions 'compression improvements' and 'image search', which are well-represented in the changeset, but the 'Analytics dashboard' component is not visible in the provided raw_summary of code changes. Verify whether the analytics dashboard changes are included in this PR. If included, the title accurately summarizes all three major features; if not included, consider updating the title to remove the analytics reference or clarifying its implementation status.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed Docstring coverage is 85.94% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/compression-improvements
⚔️ Resolve merge conflicts (beta)
  • Auto-commit resolved conflicts to branch feat/compression-improvements
  • Create stacked PR with resolved conflicts
  • Post resolved changes as copyable diffs in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
api/routes/search.py (1)

202-215: ⚠️ Potential issue | 🟡 Minor

Filtering source screenshot may return fewer results than limit.

The source screenshot is removed at line 203–204 after results are already trimmed by date filters (line 189–200) but before the final request.limit slice at line 215. If the source screenshot was in the results, the user gets limit - 1 results. Consider requesting search_limit + 1 when searching by image ID, or moving the filter before the date-range pass.

Proposed fix: bump search_limit when searching by ID
-    search_limit = request.limit * 3 if (request.start_date or request.end_date) else request.limit
+    search_limit = request.limit * 3 if (request.start_date or request.end_date) else request.limit
+    # Request one extra result to account for filtering out the source screenshot
+    if isinstance(request.image, int):
+        search_limit += 1
web/src/lib/api.ts (1)

109-123: ⚠️ Potential issue | 🟡 Minor

searchWithParams and the SearchParams interface don't support the image parameter.

The search function was updated to accept image (line 91), but searchWithParams doesn't forward it. More importantly, the SearchParams interface (in web/src/types/index.ts) doesn't include an image field, so this API is incomplete for image-based search. To align searchWithParams with search, add image?: number | string to the SearchParams interface and include it in the request body.

web/src/app/page.tsx (1)

506-513: ⚠️ Potential issue | 🟠 Major

Pressing Escape during image search creates UI inconsistency, but the proposed fix has a critical flaw.

The bug is real: when image search is active (query contains [find-similar] or [image: filename]), pressing Escape clears the query via setQuery('') but leaves imageSearchImage and imageSearchPreview active. This causes:

  • Grid shows gallerySnapshots (line 1560: query.trim() ? searchResults : gallerySnapshots)
  • Search bar still displays "N similar results" (line 1547: query.trim() || imageSearchPreview)
  • Gallery fetch effect triggers (line 314: !query.trim())

However, the proposed fix will not work due to a stale closure bug. The keydown useEffect dependency array (line 589) omits imageSearchPreview, imageSearchImage, and clearImageSearch. Checking imageSearchPreview in the handler would use a stale value from when the effect was last created.

Correct fix: Add the missing dependencies to the keydown useEffect array:

  }, [activeView, selectedImage, query, totalSnapshots, selection, incognitoStatus.active, navigateTimeline, windowOffset, snapshots.length, loadWindowAtOffset, imageSearchPreview, imageSearchImage, clearImageSearch]);

Then the proposed Escape handler logic becomes valid:

  if (e.key === 'Escape') {
    if (selectedImage) setSelectedImage(null);
    else if (selection.selectedIds.size > 0) selection.clearSelection();
+   else if (imageSearchPreview) clearImageSearch();
    else if (query) setQuery('');
    return;
  }
🤖 Fix all issues with AI agents
In `@core/tests/test_compression.py`:
- Around line 286-292: The first offset assertion is tautological and the second
is fragile; replace both with explicit checks against the recorded call kwargs
from mock_db.get_force_recompressible_screenshots.call_args_list (the local
variable calls) so you assert calls[0].kwargs["offset"] == 0 (falling back to
examining positional args in calls[0].args if kwargs missing) and assert
calls[1].kwargs["offset"] > calls[0].kwargs["offset"] (or equals 30 if you
expect that exact value), ensuring you consistently read offset from call.kwargs
before falling back to call.args to avoid the always-true or brittle positional
checks.

In `@web/src/app/page.tsx`:
- Around line 1009-1025: Before creating a new preview in handleImageSearch,
revoke any existing blob URL (call clearImageSearch() or
URL.revokeObjectURL(prevPreview) using the current imageSearchPreview) so the
previous object URL is not leaked; then create the new URL and
setImageSearchPreview as before. Also replace the silent early returns for
invalid type/oversize with explicit user feedback (e.g., call an existing
setImageSearchError or trigger a toast/alert) so the user knows why the file was
rejected. Ensure clearImageSearch also revokes the current preview and is
invoked on unmount or when replacing previews.

In `@web/src/app/settings/page.tsx`:
- Around line 149-160: When the age dropdown changes, reset the confirmation
state so the user must re-confirm for the new preview; in
handleForceRecompressAgeChange (which already calls setForceRecompressAge,
previewForceRecompress, setForceRecompressPreview and setForceRecompressLoading)
add a call to setForceRecompressConfirmed(false) (and ensure any related UI
checkbox/state that represents confirmation is cleared) before or immediately
after fetching the new preview so the previous confirmation cannot persist
across age changes.

In `@web/src/components/CompressionSuggestion.tsx`:
- Around line 96-98: The copy in CompressionSuggestion.tsx hardcodes "older than
2 months" which can be out of sync with the actual compression threshold; update
the component to derive the text from the real configuration (e.g., use the
compression.after_days value passed into props or read from the settings object
used elsewhere) and render a dynamic phrase (convert after_days to months or
show days) instead of the hardcoded string; update any JSX that references
stats.compressible_count to combine with the dynamic threshold text so the UI
always reflects compression.after_days.
🧹 Nitpick comments (8)
api/routes/search.py (2)

72-95: Base64 decode error detail may leak internal info on line 95.

Line 95 includes {e} in the HTTP response detail, which could expose internal paths or library error messages. For a local/personal app this is low risk, but worth noting.

Also, consider adding a size check on the decoded image_bytes before passing to get_image_embedding_from_bytes to guard against excessively large payloads consuming GPU/CPU memory.

Proposed size guard
             try:
                 image_bytes = base64.b64decode(request.image)
             except Exception as e:
                 raise HTTPException(status_code=400, detail="Invalid base64 image data") from e
+
+            # Guard against excessively large uploads (e.g. 20MB)
+            if len(image_bytes) > 20 * 1024 * 1024:
+                raise HTTPException(status_code=400, detail="Image too large (max 20MB)")
+
             from core.embeddings import get_image_embedding_from_bytes

98-119: When image is provided, text query is silently ignored.

If a user sends both query="blue shirt" and image=<some_id>, the query is completely discarded — no CLIP text embedding (line 98) and no BGE text embedding (line 115) are generated. The query string is still passed to search_hybrid on line 174, which may use it for FTS, but there's no documentation of this behavior.

Consider noting this in the endpoint docstring to set expectations.

api/schemas.py (1)

211-214: Verify Pydantic union coercion behavior for int | str | None.

With image: int | str | None, Pydantic v2's smart union mode should correctly distinguish a JSON number (→ int, screenshot ID) from a JSON string (→ str, base64 data). However, if a client sends a numeric string like "123" intending it as a screenshot ID, it will be treated as a base64 string and fail at decode. Consider documenting this contract clearly in the field description.

#!/bin/bash
# Check if there's any Pydantic model_config that might affect union behavior
rg -n "model_config" --type=py -C3
rg -n "smart_union\|union_mode" --type=py -C3
web/src/app/settings/page.tsx (1)

439-516: Modal lacks keyboard dismiss and focus trapping.

The force recompress modal doesn't handle Escape key to close, and doesn't trap focus within the dialog. For a destructive operation modal, this is a minor accessibility gap.

web/src/components/CompressionSuggestion.tsx (1)

9-14: formatBytes can produce undefined unit for very large values.

If bytes is ≥ 1 TB, i will be 4, exceeding the units array bounds. While unlikely for estimated savings, a guard would be safer.

🛡️ Suggested guard
 function formatBytes(bytes: number): string {
   if (bytes === 0) return '0 B';
   const units = ['B', 'KB', 'MB', 'GB'];
-  const i = Math.floor(Math.log(bytes) / Math.log(1024));
+  const i = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1);
   return `${(bytes / Math.pow(1024, i)).toFixed(i > 1 ? 1 : 0)} ${units[i]}`;
 }
web/src/lib/api.ts (1)

82-107: Search function signature is growing – consider an options object.

The search function now has 9 positional parameters. This makes call sites fragile and hard to read. Consider migrating to an options/params object pattern, similar to searchWithParams.

core/compression.py (1)

203-208: Missing return type annotation.

start_force_recompress returns bool but the signature lacks a return type hint. As per coding guidelines, Python 3.10+ with type hints should be used.

🔧 Proposed fix
     def start_force_recompress(
         self,
         older_than_days: int,
         quality: int | None = None,
         on_progress: Callable[[CompressionProgress], None] | None = None,
-    ):
+    ) -> bool:
         """Start force recompression in background thread"""
web/src/app/page.tsx (1)

1558-1561: itemsToShow relies on synthetic query to select search results — fragile coupling.

The expression query.trim() ? searchResults : gallerySnapshots works during image search only because query is set to a synthetic string. If the query gets cleared independently (e.g., via the Escape key bug noted above, or future refactoring), this silently falls back to gallery items while image search results are still populated.

Proposed fix — decouple from synthetic query
- const itemsToShow = query.trim() ? searchResults : gallerySnapshots;
+ const itemsToShow = (query.trim() || imageSearchImage !== null) ? searchResults : gallerySnapshots;

Comment on lines +286 to +292
# Verify offset-based calls
calls = mock_db.get_force_recompressible_screenshots.call_args_list
assert len(calls) == 3
# First call: offset=0
assert calls[0].kwargs.get("offset", calls[0][1][2] if len(calls[0][1]) > 2 else 0) == 0 or calls[0] == calls[0]
# Check that offset increases
assert calls[1][1] == (30,) or calls[1].kwargs.get("offset", 0) > 0

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Tautological assertion – offset verification always passes.

Line 290's assertion has or calls[0] == calls[0] which is always True, so the assertion never fails regardless of the actual offset value. Line 292 is similarly weak — calls[1][1] == (30,) checks positional args which may or may not match the actual call shape.

Consider asserting on the actual keyword arguments directly:

🔧 Proposed fix
-        # First call: offset=0
-        assert calls[0].kwargs.get("offset", calls[0][1][2] if len(calls[0][1]) > 2 else 0) == 0 or calls[0] == calls[0]
-        # Check that offset increases
-        assert calls[1][1] == (30,) or calls[1].kwargs.get("offset", 0) > 0
+        # Verify offset increases across calls
+        offsets = [c.kwargs.get("offset", 0) for c in calls]
+        assert offsets[0] == 0
+        assert offsets[1] > 0
+        assert offsets[2] > offsets[1]
🤖 Prompt for AI Agents
In `@core/tests/test_compression.py` around lines 286 - 292, The first offset
assertion is tautological and the second is fragile; replace both with explicit
checks against the recorded call kwargs from
mock_db.get_force_recompressible_screenshots.call_args_list (the local variable
calls) so you assert calls[0].kwargs["offset"] == 0 (falling back to examining
positional args in calls[0].args if kwargs missing) and assert
calls[1].kwargs["offset"] > calls[0].kwargs["offset"] (or equals 30 if you
expect that exact value), ensuring you consistently read offset from call.kwargs
before falling back to call.args to avoid the always-true or brittle positional
checks.

Comment thread web/src/app/page.tsx
Comment on lines +1009 to +1025
const handleImageSearch = useCallback((file: File) => {
if (!file.type.startsWith('image/')) return;
if (file.size > 10 * 1024 * 1024) return;

const previewUrl = URL.createObjectURL(file);
setImageSearchPreview(previewUrl);
setQuery(`[image: ${file.name}]`);
setActiveView('search');

// Read file as base64 — the search effect handles the actual API call
const reader = new FileReader();
reader.onload = () => {
const base64 = (reader.result as string).split(',')[1];
setImageSearchImage(base64);
};
reader.readAsDataURL(file);
}, []);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Blob URL leak when a second image is dropped without clearing the first.

When handleImageSearch is called while there's already an active blob-URL preview (e.g., user drags a second image), the previous URL.createObjectURL is never revoked — clearImageSearch is not called, and the callback has no reference to the prior URL.

Additionally, the silent return on invalid type / oversized file gives no user feedback.

Proposed fix
- const handleImageSearch = useCallback((file: File) => {
-   if (!file.type.startsWith('image/')) return;
-   if (file.size > 10 * 1024 * 1024) return;
+ const handleImageSearch = useCallback((file: File) => {
+   if (!file.type.startsWith('image/')) {
+     console.warn('Unsupported file type:', file.type);
+     return;
+   }
+   if (file.size > 10 * 1024 * 1024) {
+     console.warn('File too large (max 10 MB):', file.size);
+     return;
+   }
+
+   // Revoke previous blob URL to avoid memory leak
+   if (imageSearchPreview && imageSearchPreview.startsWith('blob:')) {
+     URL.revokeObjectURL(imageSearchPreview);
+   }
 
    const previewUrl = URL.createObjectURL(file);
    setImageSearchPreview(previewUrl);
    ...
- }, []);
+ }, [imageSearchPreview]);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const handleImageSearch = useCallback((file: File) => {
if (!file.type.startsWith('image/')) return;
if (file.size > 10 * 1024 * 1024) return;
const previewUrl = URL.createObjectURL(file);
setImageSearchPreview(previewUrl);
setQuery(`[image: ${file.name}]`);
setActiveView('search');
// Read file as base64 — the search effect handles the actual API call
const reader = new FileReader();
reader.onload = () => {
const base64 = (reader.result as string).split(',')[1];
setImageSearchImage(base64);
};
reader.readAsDataURL(file);
}, []);
const handleImageSearch = useCallback((file: File) => {
if (!file.type.startsWith('image/')) {
console.warn('Unsupported file type:', file.type);
return;
}
if (file.size > 10 * 1024 * 1024) {
console.warn('File too large (max 10 MB):', file.size);
return;
}
// Revoke previous blob URL to avoid memory leak
if (imageSearchPreview && imageSearchPreview.startsWith('blob:')) {
URL.revokeObjectURL(imageSearchPreview);
}
const previewUrl = URL.createObjectURL(file);
setImageSearchPreview(previewUrl);
setQuery(`[image: ${file.name}]`);
setActiveView('search');
// Read file as base64 — the search effect handles the actual API call
const reader = new FileReader();
reader.onload = () => {
const base64 = (reader.result as string).split(',')[1];
setImageSearchImage(base64);
};
reader.readAsDataURL(file);
}, [imageSearchPreview]);
🤖 Prompt for AI Agents
In `@web/src/app/page.tsx` around lines 1009 - 1025, Before creating a new preview
in handleImageSearch, revoke any existing blob URL (call clearImageSearch() or
URL.revokeObjectURL(prevPreview) using the current imageSearchPreview) so the
previous object URL is not leaked; then create the new URL and
setImageSearchPreview as before. Also replace the silent early returns for
invalid type/oversize with explicit user feedback (e.g., call an existing
setImageSearchError or trigger a toast/alert) so the user knows why the file was
rejected. Ensure clearImageSearch also revokes the current preview and is
invoked on unmount or when replacing previews.

Comment on lines +149 to +160
const handleForceRecompressAgeChange = async (days: number) => {
setForceRecompressAge(days);
setForceRecompressLoading(true);
try {
const preview = await previewForceRecompress(days);
setForceRecompressPreview(preview);
} catch (err) {
console.error('Failed to preview:', err);
} finally {
setForceRecompressLoading(false);
}
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Confirmation checkbox is not reset when age changes.

When the user changes the age dropdown, the preview data updates but forceRecompressConfirmed retains its previous value. This means a user could confirm for "90 days" and then change to "30 days" (affecting a different set of screenshots) without re-confirming.

🔧 Proposed fix
   const handleForceRecompressAgeChange = async (days: number) => {
     setForceRecompressAge(days);
+    setForceRecompressConfirmed(false);
     setForceRecompressLoading(true);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const handleForceRecompressAgeChange = async (days: number) => {
setForceRecompressAge(days);
setForceRecompressLoading(true);
try {
const preview = await previewForceRecompress(days);
setForceRecompressPreview(preview);
} catch (err) {
console.error('Failed to preview:', err);
} finally {
setForceRecompressLoading(false);
}
};
const handleForceRecompressAgeChange = async (days: number) => {
setForceRecompressAge(days);
setForceRecompressConfirmed(false);
setForceRecompressLoading(true);
try {
const preview = await previewForceRecompress(days);
setForceRecompressPreview(preview);
} catch (err) {
console.error('Failed to preview:', err);
} finally {
setForceRecompressLoading(false);
}
};
🤖 Prompt for AI Agents
In `@web/src/app/settings/page.tsx` around lines 149 - 160, When the age dropdown
changes, reset the confirmation state so the user must re-confirm for the new
preview; in handleForceRecompressAgeChange (which already calls
setForceRecompressAge, previewForceRecompress, setForceRecompressPreview and
setForceRecompressLoading) add a call to setForceRecompressConfirmed(false) (and
ensure any related UI checkbox/state that represents confirmation is cleared)
before or immediately after fetching the new preview so the previous
confirmation cannot persist across age changes.

Comment on lines +96 to +98
<p className="text-xs text-[#666] mt-1.5 leading-relaxed">
You have <span className="text-[#86efac] font-medium">{stats.compressible_count.toLocaleString()}</span> screenshots older than 2 months that can be compressed. Search and embeddings stay untouched.
</p>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Hardcoded "older than 2 months" may not match actual config.

The text says "screenshots older than 2 months" but the actual compression.after_days config value could differ (the default was changed across commits — 90 then 60). If the user or a future commit changes the threshold, this text becomes misleading.

Consider either fetching the config value or deriving the text from the actual after_days setting.

🤖 Prompt for AI Agents
In `@web/src/components/CompressionSuggestion.tsx` around lines 96 - 98, The copy
in CompressionSuggestion.tsx hardcodes "older than 2 months" which can be out of
sync with the actual compression threshold; update the component to derive the
text from the real configuration (e.g., use the compression.after_days value
passed into props or read from the settings object used elsewhere) and render a
dynamic phrase (convert after_days to months or show days) instead of the
hardcoded string; update any JSX that references stats.compressible_count to
combine with the dynamic threshold text so the UI always reflects
compression.after_days.

@VedankPurohit
VedankPurohit merged commit 9e69734 into main Feb 15, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant