mirror of
https://github.com/wahyd4/hey-search.git
synced 2026-08-08 21:05:14 +10:00
Add screenshots
This commit is contained in:
@@ -1,8 +1,30 @@
|
|||||||
# HeySearch
|
# HeySearch
|
||||||
|
|
||||||
A privacy-respecting [metasearch engine](https://en.wikipedia.org/wiki/Metasearch_engine) that aggregates results from multiple search engines.
|
A privacy-respecting [metasearch engine](https://en.wikipedia.org/wiki/Metasearch_engine) that aggregates results from multiple search engines — with a modern UI designed for humans and AI agents alike.
|
||||||
|
|
||||||
Inspired by [SearXNG](https://github.com/searxng/searxng).
|
> Inspired by [SearXNG](https://github.com/searxng/searxng), built for the modern web.
|
||||||
|
|
||||||
|
## Screenshots
|
||||||
|
|
||||||
|
| Home | Web Search | Image Search |
|
||||||
|
|------|-----------|--------------|
|
||||||
|
| [](screenshots/home-page.png) | [](screenshots/text-search.png) | [](screenshots/image-search.png) |
|
||||||
|
|
||||||
|
## Why HeySearch over SearXNG?
|
||||||
|
|
||||||
|
Both are open-source, self-hosted, privacy-respecting metasearch engines. Here's why HeySearch is the better choice for most people:
|
||||||
|
|
||||||
|
| | HeySearch | SearXNG |
|
||||||
|
|---|---|---|
|
||||||
|
| **Setup** | `docker run -p 8000:8000 ghcr.io/…/hey-search` — one command, zero config | Requires YAML config, engine tuning, often breaks out of the box |
|
||||||
|
| **UI** | Modern, clean React UI with dark mode, background images, image lightbox | Functional but dated — not optimised for mobile or daily use |
|
||||||
|
| **AI agent friendly** | Clean JSON REST API, OpenAPI docs at `/docs`, designed to be queried programmatically | API exists but less documented; HTML-heavy responses |
|
||||||
|
| **Bookmarks** | Built-in bookmark manager for results | ❌ |
|
||||||
|
| **Search history** | Full search history with timestamps, re-run any past query in one click | ❌ |
|
||||||
|
| **Usage stats** | Built-in analytics dashboard — top queries, click-through rates, engine usage | ❌ |
|
||||||
|
| **Background gallery** | Beautiful Unsplash/Picsum backgrounds on the home page | ❌ |
|
||||||
|
|
||||||
|
**TL;DR** — If you want something you can run in 30 seconds, looks great, works well on your phone, and exposes a clean API for your AI tools, HeySearch is for you. If you need 70+ search engines and deep customisation, SearXNG has the edge.
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
@@ -19,36 +41,7 @@ Inspired by [SearXNG](https://github.com/searxng/searxng).
|
|||||||
|
|
||||||
## Running Locally
|
## Running Locally
|
||||||
|
|
||||||
### 1. Backend
|
### 1. Quick start
|
||||||
|
|
||||||
```bash
|
|
||||||
cd backend
|
|
||||||
uv sync # install dependencies
|
|
||||||
uv run uvicorn app.main:app --reload --port 8000
|
|
||||||
```
|
|
||||||
|
|
||||||
To enable Redis caching:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
REDIS_URL=redis://192.168.1.2:6399 uv run uvicorn app.main:app --reload --port 8000
|
|
||||||
```
|
|
||||||
|
|
||||||
The API will be available at `http://localhost:8000`. Interactive docs:
|
|
||||||
|
|
||||||
- Swagger UI: http://localhost:8000/docs
|
|
||||||
- Redoc: http://localhost:8000/redoc
|
|
||||||
|
|
||||||
### 2. Frontend
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd frontend
|
|
||||||
npm install # install dependencies
|
|
||||||
npm run dev # start dev server with hot reload
|
|
||||||
```
|
|
||||||
|
|
||||||
The frontend dev server runs at `http://localhost:5173` and proxies `/api` requests to the backend.
|
|
||||||
|
|
||||||
### 3. Quick start (both)
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Without Redis (caching disabled)
|
# Without Redis (caching disabled)
|
||||||
@@ -58,7 +51,7 @@ just dev
|
|||||||
REDIS_URL=redis://192.168.1.2:6399 just dev
|
REDIS_URL=redis://192.168.1.2:6399 just dev
|
||||||
```
|
```
|
||||||
|
|
||||||
### 4. Docker (production)
|
### 2. Docker (production)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker build -t hey-search .
|
docker build -t hey-search .
|
||||||
|
|||||||
@@ -557,7 +557,7 @@ function App() {
|
|||||||
|
|
||||||
{/* Category / filter bar */}
|
{/* Category / filter bar */}
|
||||||
<div className="sticky top-[57px] z-30 border-b bg-background/95 backdrop-blur">
|
<div className="sticky top-[57px] z-30 border-b bg-background/95 backdrop-blur">
|
||||||
<nav aria-label="Search categories" className="flex items-center gap-1 px-4 py-2">
|
<nav aria-label="Search categories" className="flex items-center gap-1 px-4 py-2 overflow-x-auto [&::-webkit-scrollbar]:hidden [-ms-overflow-style:none] [scrollbar-width:none]">
|
||||||
{([
|
{([
|
||||||
{ key: "web" as const, label: "Web", icon: Globe },
|
{ key: "web" as const, label: "Web", icon: Globe },
|
||||||
{ key: "images" as const, label: "Images", icon: ImageIcon },
|
{ key: "images" as const, label: "Images", icon: ImageIcon },
|
||||||
@@ -567,7 +567,7 @@ function App() {
|
|||||||
onClick={() => handleCategoryChange(key)}
|
onClick={() => handleCategoryChange(key)}
|
||||||
aria-current={category === key ? "page" : undefined}
|
aria-current={category === key ? "page" : undefined}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex items-center gap-1.5 rounded-full px-4 py-1.5 text-sm font-medium transition-colors focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none",
|
"shrink-0 flex items-center gap-1.5 rounded-full px-4 py-1.5 text-sm font-medium transition-colors focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none",
|
||||||
category === key
|
category === key
|
||||||
? "bg-primary text-primary-foreground"
|
? "bg-primary text-primary-foreground"
|
||||||
: "text-muted-foreground hover:bg-accent"
|
: "text-muted-foreground hover:bg-accent"
|
||||||
@@ -581,15 +581,15 @@ function App() {
|
|||||||
{/* Image size filter — only visible in images category */}
|
{/* Image size filter — only visible in images category */}
|
||||||
{category === "images" && (
|
{category === "images" && (
|
||||||
<>
|
<>
|
||||||
<div className="mx-2 h-5 w-px bg-border" aria-hidden="true" />
|
<div className="mx-2 h-5 w-px shrink-0 bg-border" aria-hidden="true" />
|
||||||
<SlidersHorizontal className="h-3.5 w-3.5 text-muted-foreground" aria-hidden="true" />
|
<SlidersHorizontal className="h-3.5 w-3.5 shrink-0 text-muted-foreground" aria-hidden="true" />
|
||||||
{IMAGE_SIZE_OPTIONS.map(({ value, label }) => (
|
{IMAGE_SIZE_OPTIONS.map(({ value, label }) => (
|
||||||
<button
|
<button
|
||||||
key={value}
|
key={value}
|
||||||
onClick={() => handleImageSizeChange(value)}
|
onClick={() => handleImageSizeChange(value)}
|
||||||
aria-pressed={imageSize === value}
|
aria-pressed={imageSize === value}
|
||||||
className={cn(
|
className={cn(
|
||||||
"rounded-full px-3 py-1 text-xs font-medium transition-colors focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none",
|
"shrink-0 rounded-full px-3 py-1 text-xs font-medium transition-colors focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none",
|
||||||
imageSize === value
|
imageSize === value
|
||||||
? "bg-secondary text-secondary-foreground"
|
? "bg-secondary text-secondary-foreground"
|
||||||
: "text-muted-foreground hover:bg-accent"
|
: "text-muted-foreground hover:bg-accent"
|
||||||
@@ -603,15 +603,15 @@ function App() {
|
|||||||
|
|
||||||
{/* Sort order — available for both web and images */}
|
{/* Sort order — available for both web and images */}
|
||||||
<>
|
<>
|
||||||
<div className="mx-2 h-5 w-px bg-border" aria-hidden="true" />
|
<div className="mx-2 h-5 w-px shrink-0 bg-border" aria-hidden="true" />
|
||||||
<span className="text-xs text-muted-foreground" aria-hidden="true">Sort:</span>
|
<span className="shrink-0 text-xs text-muted-foreground" aria-hidden="true">Sort:</span>
|
||||||
{SORT_OPTIONS.map(({ value, label }) => (
|
{SORT_OPTIONS.map(({ value, label }) => (
|
||||||
<button
|
<button
|
||||||
key={value}
|
key={value}
|
||||||
onClick={() => handleSortChange(value)}
|
onClick={() => handleSortChange(value)}
|
||||||
aria-pressed={sortOrder === value}
|
aria-pressed={sortOrder === value}
|
||||||
className={cn(
|
className={cn(
|
||||||
"rounded-full px-3 py-1 text-xs font-medium transition-colors focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none",
|
"shrink-0 rounded-full px-3 py-1 text-xs font-medium transition-colors focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none",
|
||||||
sortOrder === value
|
sortOrder === value
|
||||||
? "bg-secondary text-secondary-foreground"
|
? "bg-secondary text-secondary-foreground"
|
||||||
: "text-muted-foreground hover:bg-accent"
|
: "text-muted-foreground hover:bg-accent"
|
||||||
|
|||||||
@@ -106,7 +106,8 @@ export function AppHeader({
|
|||||||
className="shrink-0 text-xl font-bold focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none rounded"
|
className="shrink-0 text-xl font-bold focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none rounded"
|
||||||
style={{ color: "#27ae60" }}
|
style={{ color: "#27ae60" }}
|
||||||
>
|
>
|
||||||
HeySearch
|
<span className="sm:hidden">HS</span>
|
||||||
|
<span className="hidden sm:inline">HeySearch</span>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 3.1 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.1 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 386 KiB |
Reference in New Issue
Block a user