Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
459786c
feat: migrate to Gemini, optimize performance, and secure TripViewer
GehDoc Jun 14, 2026
47530e5
docs: restore full content of README.md sections
GehDoc Jun 14, 2026
43904a3
fix: use SVG icon exclusively and relocate to public folder
GehDoc Jun 14, 2026
f39d5b2
test: fix E2E golden path for Gemini migration and UI changes
GehDoc Jun 14, 2026
8d70cb5
fix: update Gemini model identifier to include models/ prefix
GehDoc Jun 14, 2026
7b9f00b
fix: resolve double models/ prefix in Gemini API URL
GehDoc Jun 14, 2026
d01aad4
fix: switch to Gemini v1 stable endpoint
GehDoc Jun 14, 2026
453e2f2
fix: move Gemini API key to x-goog-api-key header
GehDoc Jun 14, 2026
ab675e1
fix: switch to gemini-3.1-flash-lite for higher quota limits
GehDoc Jun 14, 2026
b70ec5e
cleanup: move dangling spec files to completed
GehDoc Jun 15, 2026
785042d
docs(spec): halt v1 and initialize v2 for Hugging Face pivot
GehDoc Jun 15, 2026
f5411e9
feat: pivot back to Hugging Face with enhanced inference and factual …
GehDoc Jun 15, 2026
05061d5
feat: implement strict markdown sanitization and fix hydration errors
GehDoc Jun 15, 2026
4b89220
feat: finalize system prompt with localized map links and top-level p…
GehDoc Jun 15, 2026
9c43bc6
chore: bump version to 0.3.0
GehDoc Jun 15, 2026
c4b0e5b
docs: archive specifications for v0.3.0
GehDoc Jun 15, 2026
1368485
test: update E2E tests for Hugging Face and v0.3.0 UI
GehDoc Jun 15, 2026
34e378e
chore: use dedicated port 3002 for E2E tests
GehDoc Jun 15, 2026
4bc3b2a
fix: robust Google Maps URL encoding in UI and prompt
GehDoc Jun 15, 2026
061b937
docs: finalize refinement spec archival
GehDoc Jun 15, 2026
e2661ec
show the start of the journey
GehDoc Jun 15, 2026
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
11 changes: 11 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ Thank you for your interest in contributing! We follow a **Spec-Driven Developme
## πŸ’» Coding Standards

- **No Inline Styles**: To ensure maintainability and style consistency, inline styles (`style={{ ... }}`) are prohibited in production components. Use Tailwind classes exclusively.
- **Markdown Security**: All AI-generated content MUST be passed through the sanitization pipeline in `TripViewer.tsx`. Never use `dangerouslySetInnerHTML` or disable sanitization without a security review. Any new component added to the markdown renderer must be explicitly whitelisted in the sanitization schema.

## πŸ”„ Workflow & Automation

Expand Down Expand Up @@ -57,6 +58,16 @@ If working without an agent, follow these steps to keep the project state synchr
| `npm run test:e2e` | Run E2E tests with Playwright |
| `npm run type-check` | Validate TypeScript types |

## πŸ“¦ Deployment

This project is optimized for static hosting on **GitHub Pages**:

```bash
npm run deploy
```

_Note: This command runs `next build` with `output: 'export'` and pushes the `out` directory to the `gh-pages` branch._

## πŸ§ͺ Testing Strategy

Beyond end-to-end testing, we use a multi-tiered strategy for component, accessibility, and visual validation:
Expand Down
55 changes: 32 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,44 @@
[![Framework](https://img.shields.io/badge/Built_with-React-61DAFB?logo=react)](https://react.dev)
[![AI](https://img.shields.io/badge/AI_Model-Llama_3-7B1FA2?logo=meta)](https://huggingface.co)

**Quick-tripper** is a privacy-first, zero-backend travel companion web app that generates detailed road trip itineraries using AI. It operates entirely in your browserβ€”no databases, no logins, no tracking.
**Quick-tripper** is a privacy-first, zero-backend travel companion web app that generates detailed road trip itineraries using AI. It operates entirely in your browserβ€”no databases, no logins, no tracking. Now powered by **Llama 3.1** via the Hugging Face Serverless API.

## ✨ Features

- **🌍 Smart Itineraries**: Generate day-by-day travel plans using Llama 3 via the Hugging Face Router.
- **πŸ—ΊοΈ Interactive Maps**: Embedded Google Maps for every journey, secured via client-side validation.
- **🌍 Smart Itineraries**: Generate day-by-day travel plans using Llama 3.1.
- **πŸ—ΊοΈ Interactive Maps**: Embedded Google Maps for every journey, rendered securely from AI-generated routes.
- **πŸ”’ Privacy-First**: "Bring Your Own Key" (BYOK) model. Your API token is stored locally in your browser and never sent to our servers.
- **πŸ”— Compressed Sharing**: Share your entire itinerary via a single, ultra-compressed URL (powered by LZString). No backend storage required.
- **πŸ”— Compressed Sharing**: Share your entire itinerary via a single, ultra-compressed URL (powered by LZString).
- **πŸ’Ύ Local Persistence**: Automatic synchronization with your browser's local storage.
- **🚦 Quality Control**: Pre-commit hooks via Husky and lint-staged to ensure type safety and code style.
- **πŸ§ͺ Automated Testing**: Unit and utility testing powered by Vitest and JSDOM.
- **πŸš€ Serverless Architecture**: Designed to be hosted on static platforms like GitHub Pages.
- **πŸ›‘οΈ Secure Rendering**: Strict markdown sanitization to prevent XSS and ensure your browser environment remains safe.

## πŸ“– How-to Use

1. **Enter your Hugging Face API Token**: Get a free "User Access Token" from your [Hugging Face Settings](https://huggingface.co/settings/tokens).
2. **Describe your trip**: Use the prompt area to describe where you want to go, for how long, and what you like.
3. **Generate**: Click "Generate Itinerary" and wait a few seconds.
4. **Explore**: Use the interactive map and read the day-by-day suggestions.
5. **Manage**: Your trips are saved automatically. You can export them as JSON or share them via a unique link.

## πŸ”’ Privacy & BYOK (Bring Your Own Key)

Quick-tripper is designed to be **serverless and private**. We don't have a backend to store your data or your keys.

- **Local Storage**: Your API token and trip history are stored ONLY in your browser's local storage.
- **Direct AI Calls**: The app communicates directly with Hugging Face APIs from your browser.
- **No Tracking**: We don't use cookies, analytics, or tracking scripts.

To use the app, you need a **Hugging Face User Access Token**.

- Visit [Hugging Face Settings](https://huggingface.co/settings/tokens).
- Create a new token (Read access is enough).
- Paste it into the top bar of Quick-tripper.

## πŸ› οΈ Tech Stack

- **Framework**: [Next.js 15+](https://nextjs.org) (App Router)
- **Styling**: [TailwindCSS 4](https://tailwindcss.com) + [DaisyUI 5](https://daisyui.com)
- **AI Integration**: [Hugging Face Router](https://huggingface.co/docs/hub/spaces-router) (OpenAI-compatible)
- **AI Integration**: [Llama 3.1 8B](https://huggingface.co/meta-llama/Llama-3.1-8B-Instruct) via Hugging Face Router.
- **Data Compression**: [LZString](https://pieroxy.net/lua/lz-string/index.html)
- **Testing**: [Vitest](https://vitest.dev) + [React Testing Library](https://testing-library.com)
- **Automation**: [Husky](https://typicode.github.io/husky/) + [lint-staged](https://github.com/lint-staged/lint-staged)
Expand All @@ -37,21 +57,20 @@ This project follows **Spec-Driven Development (SDD)** to maintain a clear roadm

## πŸš€ Getting Started

1. **Get an API Key**: Obtain a free User Access Token from [Hugging Face Settings](https://huggingface.co/settings/tokens).
2. **Clone the Repo**:
1. **Clone the Repo**:
```bash
git clone https://github.com/gehdoc/quick-tripper.git
cd quick-tripper
```
3. **Install Dependencies**:
2. **Install Dependencies**:
```bash
npm install
```
4. **Run Locally**:
3. **Run Locally**:
```bash
npm run dev
```
5. **Open the App**: Visit `http://localhost:3000` (development) or `http://localhost:3000/quick-tripper` (production-emulated) and enter your API key in the top bar.
4. **Open the App**: Visit `http://localhost:3000` (development) or `http://localhost:3000/quick-tripper` (production-emulated) and enter your API key in the top bar.

## πŸ—οΈ Architecture

Expand All @@ -68,16 +87,6 @@ To maintain a robust "zero-backend" application, we rely on a multi-tiered valid
5. **E2E Testing**: Playwright validates full user workflows and "golden paths" in a real browser environment.
6. **Spec-First Implementation**: Every change is traced back to a technical specification in `specs/`, ensuring architectural alignment.

## πŸ“¦ Deployment

This project is optimized for static hosting on **GitHub Pages**:

```bash
npm run deploy
```

_Note: This command runs `next build` with `output: 'export'` and pushes the `out` directory to the `gh-pages` branch._

## πŸ“„ License

MIT Β© [Quick-tripper](https://github.com/gehdoc/quick-tripper)
Expand Down
23 changes: 23 additions & 0 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,29 @@ When the `Trip` data structure changes in a breaking way, follow these steps to
5. **Test**:
- Add test cases in `src/tests/migration.test.ts` to verify the upgrade path from `v<N-1>` to `v<N>`.

## AI Integration

The application uses **Llama 3.1 (8B)** for itinerary generation, accessed via the Hugging Face Serverless API.

### Structured Output

We enforce a JSON schema through strict system prompts. This allows us to reliably extract:

- `title`: A clean summary of the trip.
- `start`/`stop`: Geocodable endpoints for map rendering and future filtering.
- `content`: The markdown-formatted itinerary.

## Security Architecture

### Markdown Sanitization (XSS Prevention)

To maintain user safety in a BYOK (Bring Your Own Key) environment, the application implements a strict **Whitelist-Only** sanitization policy for AI-generated content.

1. **Strict Whitelisting**: We use `rehype-sanitize` to strip all potentially dangerous HTML tags (`<script>`, `<object>`, `<embed>`, `<form>`, etc.) and attributes (event handlers like `onclick`).
2. **Iframe Protection**: Raw `<iframe>` tags from the AI are blocked entirely.
3. **Managed Map Rendering**: Iframes are ONLY permitted when generated by our internal `TripViewer` logic, which converts verified Google Maps URLs into secure embedded views.
4. **No Tracking**: By blocking `<img>` tags and external CSS, we prevent the AI from including tracking pixels or malicious styling.

## Component Architecture

The project follows a **Smart Container & Dumb Presenter** pattern (Clean Architecture) to ensure high testability and separation of concerns.
Expand Down
31 changes: 21 additions & 10 deletions e2e/golden-path.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,41 @@ import { test, expect } from '@playwright/test';

test('golden path - generate trip', async ({ page }) => {
await page.goto('/');
// Mock API
await page.route('https://router.huggingface.co/v1/chat/completions', (route) =>

// Mock Hugging Face API (OpenAI compatible)
await page.route('**/v1/chat/completions', (route) =>
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
choices: [
{
message: {
content: '# Golden Trip\nThis is a mocked itinerary.',
content: JSON.stringify({
title: 'Golden Trip',
start: 'Zurich',
stop: 'Interlaken',
content: '### Golden Trip\nThis is a mocked itinerary.',
}),
},
},
],
}),
}),
);

// Enter API Key
await page.getByPlaceholder('HuggingFace API Token').fill('dummy-key');
// Enter Destination
await page.getByPlaceholder('Ex: A 4-day hike').fill('Swiss Alps');
// Enter API Key (Restored Hugging Face placeholder)
await page.getByPlaceholder('HuggingFace API Token').fill('hf_dummy-key');

// Enter Prompt (Textarea)
await page.getByPlaceholder(/Ex: A 4-day hike itinerary/i).fill('Swiss Alps');

// Send
await page.locator('.join').getByRole('button').click(); // Send icon button within join component
await page.getByRole('button', { name: /Generate Itinerary/i }).click();

// Verify Heading (Rendered in Navigator) - Using .first() or specific role to avoid ambiguity
await expect(page.getByRole('heading', { name: 'Golden Trip' })).toBeVisible();

// Verify
await expect(page.getByText('Golden Trip')).toBeVisible();
// Verify Content
await expect(page.getByText('This is a mocked itinerary.')).toBeVisible();
});
36 changes: 33 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "quick-tripper",
"type": "module",
"version": "0.2.0",
"version": "0.3.0",
"private": true,
"description": "Privacy-first, zero-backend AI travel companion",
"author": "GehDoc",
Expand Down Expand Up @@ -60,7 +60,8 @@
"react-dom": "19.2.4",
"react-icons": "^5.6.0",
"react-markdown": "^10.1.0",
"rehype-raw": "^7.0.0"
"rehype-raw": "^7.0.0",
"rehype-sanitize": "^6.0.0"
},
"devDependencies": {
"@playwright/test": "^1.60.0",
Expand Down
6 changes: 3 additions & 3 deletions playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export default defineConfig({
workers: process.env.CI ? 1 : undefined,
reporter: 'list',
use: {
baseURL: 'http://localhost:3001',
baseURL: 'http://localhost:3002',
trace: 'on-first-retry',
},
projects: [
Expand All @@ -18,8 +18,8 @@ export default defineConfig({
},
],
webServer: {
command: 'npx next dev -p 3001',
url: 'http://localhost:3001',
command: 'npx next dev -p 3002',
url: 'http://localhost:3002',
reuseExistingServer: !process.env.CI,
timeout: 120000,
},
Expand Down
File renamed without changes
Loading
Loading