Skip to content

About

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

Β 

History

2 Commits

Folders and files

NameName
Last commit message
Last commit date
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

Splur β€” AI Live Chat Agent

A production-quality AI customer support chat application built for the Spur founding engineer take-home. Features a real-time chat interface with an AI agent (Luna) that answers questions about a fictional artisan home goods store (Lunara).

Tech Stack Svelte Express SQLite Groq

Features

  • πŸ’¬ Real-time chat UI β€” Polished dark/light theme with message bubbles, typing indicator, auto-scroll
  • πŸ€– AI-powered responses β€” Groq Llama 3.3 70B with contextual conversation history
  • πŸ“š FAQ knowledge base β€” Seeded domain knowledge (shipping, returns, payments, support hours)
  • πŸ’Ύ Persistent conversations β€” SQLite-backed, survives page reloads via session ID in localStorage
  • πŸ›‘οΈ Robust error handling β€” Graceful LLM failures, input validation, rate limit handling
  • 🎨 Beautiful design β€” Glass-morphism effects, gradient accents, smooth animations, responsive layout
  • ⌨️ Great UX β€” Enter to send, Shift+Enter for newline, typing indicator, suggested questions, character limit

Quick Start

Prerequisites

1. Clone & Setup Environment

git clone <your-repo-url>
cd splur

# Create environment file
cp .env.example backend/.env

Edit backend/.env and add your Groq API key:

GROQ_API_KEY=your-actual-key-here

2. Start the Backend

cd backend
npm install
npm run dev

The backend starts on http://localhost:3001. On first run, it automatically:

  • Creates the SQLite database (./data/chat.db)
  • Runs schema migrations
  • Seeds FAQ data for the Lunara store

3. Start the Frontend

cd frontend
npm install
npm run dev

The frontend starts on http://localhost:5173. Open it in your browser and start chatting!

Database Setup

No manual setup required. The database is automatically created and migrated on first backend start. FAQ data is auto-seeded when the faq_entries table is empty.

To manually re-seed FAQ data:

cd backend
npm run db:seed

Environment Variables

Variable Required Default Description
GROQ_API_KEY βœ… β€” Your Groq API key
PORT 3001 Backend server port
DATABASE_PATH ./data/chat.db SQLite database file path
MAX_TOKENS 500 Max tokens per LLM response
MAX_MESSAGE_LENGTH 2000 Max characters per user message
MAX_HISTORY_MESSAGES 20 Max messages sent as LLM context
FRONTEND_URL http://localhost:5173 CORS allowed origin
NODE_ENV development Environment mode
VITE_API_URL http://localhost:3001 Backend URL (frontend env)

Architecture

splur/
β”œβ”€β”€ backend/                    # Express + TypeScript API server
β”‚   └── src/
β”‚       β”œβ”€β”€ index.ts            # Entry point β€” Express app bootstrap
β”‚       β”œβ”€β”€ config.ts           # Zod-validated environment config
β”‚       β”œβ”€β”€ types.ts            # Shared TypeScript types
β”‚       β”œβ”€β”€ routes/
β”‚       β”‚   └── chat.ts         # Chat endpoints (POST /message, GET /history)
β”‚       β”œβ”€β”€ services/
β”‚       β”‚   └── llm.ts          # Groq integration (generateReply)
β”‚       β”œβ”€β”€ models/
β”‚       β”‚   β”œβ”€β”€ conversation.ts # Conversation CRUD
β”‚       β”‚   └── message.ts      # Message CRUD
β”‚       β”œβ”€β”€ middleware/
β”‚       β”‚   β”œβ”€β”€ validation.ts   # Zod request validation
β”‚       β”‚   └── errorHandler.ts # Global error handler
β”‚       └── db/
β”‚           β”œβ”€β”€ index.ts        # SQLite connection manager
β”‚           β”œβ”€β”€ schema.ts       # Table migrations
β”‚           └── seed.ts         # FAQ seed data
β”‚
β”œβ”€β”€ frontend/                   # SvelteKit 5 chat UI
β”‚   └── src/
β”‚       β”œβ”€β”€ routes/
β”‚       β”‚   β”œβ”€β”€ +layout.svelte  # Root layout (global CSS)
β”‚       β”‚   └── +page.svelte    # Main page with chat widget
β”‚       └── lib/
β”‚           β”œβ”€β”€ components/
β”‚           β”‚   β”œβ”€β”€ ChatWidget.svelte      # Main container
β”‚           β”‚   β”œβ”€β”€ ChatInput.svelte       # Input + send button
β”‚           β”‚   β”œβ”€β”€ MessageBubble.svelte   # User/AI message bubbles
β”‚           β”‚   └── TypingIndicator.svelte # "Luna is typing..." 
β”‚           β”œβ”€β”€ stores/
β”‚           β”‚   └── chat.ts     # Svelte stores for state management
β”‚           β”œβ”€β”€ api.ts          # Backend API client
β”‚           └── types.ts        # Frontend type definitions
β”‚
β”œβ”€β”€ .env.example
β”œβ”€β”€ .gitignore
└── README.md

Design Principles

  1. Separation of Concerns β€” Routes handle HTTP, services handle business logic, models handle data access
  2. Extensibility β€” Adding a new channel (WhatsApp, Instagram) means adding a new route that calls the same generateReply() service
  3. Encapsulated LLM β€” The entire Groq integration lives in services/llm.ts. Swapping to another provider requires changing only this file
  4. Schema-first β€” Clean relational schema with conversations β†’ messages, ready for PostgreSQL migration

Data Model

conversations                    messages
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”        β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ id (PK)              │───┐    β”‚ id (PK)                    β”‚
β”‚ created_at           β”‚   β”‚    β”‚ conversation_id (FK) ──────│
β”‚ metadata (JSON)      β”‚   └────│ sender ('user' | 'ai')     β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜        β”‚ text                       β”‚
                                β”‚ created_at                 β”‚
                                β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

faq_entries
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ id (PK)                    β”‚
β”‚ category                   β”‚
β”‚ question                   β”‚
β”‚ answer                     β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

API Endpoints

Method Endpoint Description
POST /chat/message Send a message, get AI reply
GET /chat/:sessionId/history Fetch conversation history
GET /health Health check

POST /chat/message

// Request
{ "message": "What's your return policy?", "sessionId": "optional-uuid" }

// Response  
{ "reply": "We offer a 30-day return policy...", "sessionId": "uuid" }

LLM Integration

Provider

Groq Llama-3.3-70B-Versatile β€” chosen for being blazing fast and completely free (up to generous rate limits). The integration is fully encapsulated in backend/src/services/llm.ts.

Prompting Strategy

  1. System prompt defines the agent persona (Luna) with clear behavioral guidelines
  2. FAQ knowledge base is dynamically loaded from the database and injected into the system prompt
  3. Conversation history (last 20 messages) is included for contextual responses
  4. Guardrails:
    • Agent won't make up policies not in the knowledge base
    • Redirects to human support for questions it can't answer
    • Stays on-topic (won't discuss unrelated subjects)
    • Responses capped at ~150 words for conciseness

Error Handling

  • 401 (Invalid Key): "Having trouble connecting to my brain..."
  • 429 (Rate Limited): "A bit overwhelmed with requests..."
  • 500/503 (Server Error): "Experiencing technical difficulties..."
  • Timeout (30s): "Taking too long to think..."
  • All errors surface as friendly messages in the chat UI

Cost Controls

  • MAX_TOKENS=500 caps each response
  • MAX_HISTORY_MESSAGES=20 limits context window
  • MAX_MESSAGE_LENGTH=2000 prevents oversized inputs
  • Request body limited to 10KB at Express level

Trade‑offs & "If I Had More Time…"

What I'd improve:

  • Streaming responses β€” Use SSE/WebSocket to stream LLM tokens for a more responsive feel
  • Authentication β€” Add session-based auth so conversations are tied to users
  • Rate limiting β€” Add per-IP rate limiting with Redis to prevent abuse
  • Redis caching β€” Cache FAQ data and recent conversations for faster responses
  • WebSocket β€” Replace polling with real-time bidirectional communication
  • Testing β€” Add comprehensive unit tests (Vitest) and E2E tests (Playwright)
  • RAG β€” Use vector embeddings + semantic search instead of dumping all FAQs into the prompt
  • Multi-channel β€” Add WhatsApp/Instagram webhook handlers that call the same generateReply() service
  • Admin dashboard β€” View all conversations, analytics, agent performance metrics
  • Markdown rendering β€” Parse AI responses for bold, lists, links
  • Message reactions β€” Thumbs up/down feedback on AI responses for quality tracking
  • Docker Compose β€” One-command setup for the full stack
  • CI/CD β€” GitHub Actions for lint, test, and deploy on push

Deliberate trade-offs:

  • SQLite over PostgreSQL: Simpler for the exercise, but the schema is designed to port to Postgres with zero changes to the SQL
  • All FAQs in system prompt: Works fine for a small knowledge base (~12 entries). For a larger KB, I'd use RAG with vector search
  • No auth: Keeps the scope focused. Sessions are identified by UUID stored in localStorage
  • Synchronous DB (better-sqlite3): Chosen for simplicity and speed. For production, I'd use an async driver like drizzle-orm + libsql

About

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages