Skip to content

Hoodini231/nami

Repository files navigation

Nami β€” Visual Travel Planner

Nami Logo

A visual travel planning application that lets you design itineraries using three synchronized interfaces: a custom DSL code editor, a Notion-style block editor, and an interactive canvas. Built with Next.js, React Flow, and AI-powered suggestions.


🌟 Introduction

Nami is a travel planning tool designed for people who think visually but want the precision of code. Whether you're planning a weekend getaway or a multi-week adventure, Nami gives you multiple ways to view and edit your itineraryβ€”all kept perfectly in sync.

image

Key Features

  • Three editing modes: Code (DSL), Blocks (Notion-style), Canvas (visual timeline)
  • Real-time synchronization between all editors
  • Drag-and-drop canvas with day cards, activities, and route alternatives
  • AI-powered suggestions via Gemini 2.5 Flash
  • Route swim lanes for representing alternative plans (e.g., "Hike OR Museum")
  • Resizable timeline with zoom, pan, and drag-to-reorder
  • Early 2000s aesthetic with Geocities-inspired modals

🎯 The Problem

Traditional travel planning tools force you into a single paradigm:

  • Spreadsheets are precise but hard to visualize temporal relationships
  • Google Docs become messy when representing alternative options
  • Visual planners lack the expressiveness to capture complex itineraries
  • Most tools can't handle "fork in the road" scenarios (Route A OR Route B)

When planning trips with multiple optionsβ€”like "beach day OR museum day"β€”you end up with multiple documents or confusing notation. You lose the ability to see your entire trip at a glance while maintaining the flexibility to explore alternatives.

Specific Pain Points

  1. No structured alternatives: Hard to represent "Option A vs Option B" for the same time slot
  2. Poor visual hierarchy: Can't see the flow of days at a glance
  3. Single-mode editing: Switching between "visual thinking" and "detail editing" requires different tools
  4. No AI integration: Manual planning for unfamiliar destinations is time-consuming

πŸ’‘ The Solution

Nami solves this by introducing a three-way synchronized editing experience powered by a custom DSL (Domain Specific Language) designed specifically for travel itineraries.

Architecture Overview

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  DSL Editor │────▢│  AST Store  │◀────│   Canvas    β”‚
β”‚  (Code)     β”‚     β”‚  (Source of β”‚     β”‚  (Visual)   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β”‚   Truth)    β”‚     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                    β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜
                           β”‚
                    β”Œβ”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”
                    β”‚    Block    β”‚
                    β”‚   Editor    β”‚
                    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

All three interfaces operate on the same underlying Abstract Syntax Tree (AST) representation. When you edit in any interface, the change propagates through the AST to all other views.


πŸ“ The DSL (Domain Specific Language)

Why a DSL?

Rather than using JSON or YAML, Nami uses a custom DSL that:

  • Reads like natural language but has strict structure
  • Prevents invalid states (you can't accidentally nest incompatible types)
  • Is compact and scannable (no verbose JSON boilerplate)
  • Maps cleanly to visual representation (each block = one visual node)

DSL Syntax

Day "Kyoto Temples" {
  Travel "Shinkansen to Kyoto" {
    time: "8:00 AM"
    endTime: "10:30 AM"
    from: "Tokyo Station"
    to: "Kyoto Station"
    duration: "2h 30m"
  }

  Activity "Fushimi Inari Shrine" {
    time: "11:00 AM"
    endTime: "1:00 PM"
    location: "68 Fukakusa Yabunouchicho, Fushimi Ward"
    note: "Hike the thousand torii gates. Wear comfortable shoes!"
  }

  Eat "Nishiki Market Lunch" {
    time: "1:30 PM"
    endTime: "3:00 PM"
    location: "Nishiki Market, Nakagyo Ward"
    note: "Try fresh sashimi and matcha ice cream"
  }

  Route "Temple Option A" {
    Activity "Kinkaku-ji (Golden Pavilion)" {
      time: "3:30 PM"
      endTime: "5:00 PM"
      location: "1 Kinkakujicho, Kita Ward"
      note: "Best photos in afternoon light"
    }
  }

  Route "Temple Option B" {
    Activity "Kiyomizu-dera" {
      time: "3:30 PM"
      endTime: "5:30 PM"
      location: "1-294 Kiyomizu, Higashiyama Ward"
      note: "Amazing sunset views from the terrace"
    }
  }

  Stay "Ryokan Yoshida-Sanso" {
    time: "6:00 PM"
    location: "Sakyo Ward"
    note: "Check-in, traditional kaiseki dinner at 7 PM"
  }
}

DSL Keywords

Keyword Purpose Required Fields
Day Top-level container for a day's itinerary label (name)
Activity A generic event or activity time, location
Travel Transportation between locations time, from, to
Eat A meal or food experience time, location
Stay Accommodation time, location
Note Freeform text note note
Route Alternative option (creates a swim lane) label (name)

Metadata Fields

  • time β€” Start time (e.g., "10:00 AM")
  • endTime β€” End time (optional)
  • location β€” Address or area name
  • from / to β€” Origin and destination (for Travel)
  • duration β€” Time span (for Travel)
  • note β€” Additional details, tips, or reminders
  • reservation β€” Booking confirmation info

πŸ”§ How DSL Parsing Works

Parsing Pipeline

DSL String β†’ Lexer β†’ Tokens β†’ Parser β†’ AST β†’ Layout Engine β†’ React Flow Nodes

1. Lexer (Tokenization)

The DSL string is broken into tokens using regular expressions:

const TOKEN_RE = new RegExp(
  `(\\b(?:Day|Activity|Travel|Eat|Stay|Note|Route)\\b)` +  // Keywords
  `|(\\b\\w+(?=\\s*:))` +                                    // Metadata keys
  `|("(?:[^"\\\\]|\\\\.)*")` +                              // Quoted strings
  `|([{}])` +                                                // Braces
  `|([^"{}\\s]+|\\s+)`,                                     // Everything else
  "g"
);

2. Parser (AST Construction)

The parser (parseDsl in lib/dsl-parser.ts) builds a tree structure:

interface ParsedDay {
  id: string;
  name: string;
  items: ParsedItem[];
}

interface ParsedItem {
  id: string;
  type: ItemType;  // "activity" | "travel" | "eat" | "stay" | "note" | "route"
  label: string;
  metadata: Record<string, string>;
  children?: ParsedItem[];  // For Route nodes
}

Key parsing logic:

  • Stack-based tracking for nested blocks
  • Automatic ID generation for each node
  • Metadata accumulation within blocks
  • Special handling for Route blocks (children become alternatives)

3. Layout Engine

The buildFlow function converts AST to React Flow nodes with:

  • Spatial positioning based on time and hierarchy
  • Size calculation for day cards and activity nodes
  • Route grouping (alternatives rendered as vertical swim lanes)
  • Edge generation (connecting days with decorative rope edges)
function buildFlow(days: ParsedDay[], config: LayoutConfig): {
  nodes: Node[];
  edges: Edge[];
} {
  // For each day:
  //   1. Create day card node (parent container)
  //   2. Position activity nodes inside day card
  //   3. Group Route nodes as swim lanes
  //   4. Calculate card height based on content
  //   5. Position next day card with gap

  // Generate edges connecting day cards
}

Layout modes:

  • horizontal: Days flow left-to-right (timeline-style)
  • vertical: Days stack top-to-bottom (document-style)

🎨 Canvas Node Interaction

Node Hierarchy

DayNode (Container)
β”œβ”€β”€ ActivityNode (Direct child)
β”œβ”€β”€ ActivityNode (Direct child)
β”œβ”€β”€ RouteGroupNode (Visual grouping wrapper)
β”‚   β”œβ”€β”€ RouteNode (Swim lane 1)
β”‚   β”‚   └── ActivityNode
β”‚   └── RouteNode (Swim lane 2)
β”‚       └── ActivityNode
└── ActivityNode (Direct child)

Drag-and-Drop System

Nami implements a custom drag-and-drop system with two modes:

1. Within-Day Sorting

When dragging an activity within a day:

  • System identifies "entities" (individual activities or route blocks)
  • Calculates target insertion position based on drag cursor
  • Visually shifts sibling nodes to show drop location
  • On drop, updates AST and re-serializes DSL
// Sort state tracks entities and their visual positions
type SortEntity = {
  entityId: string;
  nodeIds: string[];         // Nodes in this entity
  offsets: Record<string, number>;  // Relative positions
  pos: number;               // Current position on sort axis
  size: number;              // Total span
};

2. Cross-Day Move

When dragging an activity to a different day:

  • System detects hover over target day card
  • Inserts a ghost node at insertion point
  • Shifts target day's children to show gap
  • Expands target day card to accommodate new content
  • On drop, moves AST node from source day to target day
// Ghost node shows drop location
const ghostNode: Node = {
  id: "__cross-day-ghost__",
  type: "activityNode",
  position: { x: ghostPos, y: ghostCrossPos },
  style: {
    opacity: 0.3,
    border: "2.5px dashed rgba(196,98,45,0.6)",
    background: "rgba(196,98,45,0.06)",
  },
  draggable: false,
};

Double-Click Editing

Double-clicking any activity node opens an inline editor modal:

const onNodeDoubleClick = (node: Node) => {
  if (node.type === "activityNode") {
    setEditData({
      nodeId: node.id,
      kind: "activity",
      type: node.data.type,  // activity | travel | eat | stay
      label: node.data.label,
      metadata: node.data.metadata,
    });
  }
};

The editor modal allows changing:

  • Item type (e.g., Activity β†’ Eat)
  • Label text
  • All metadata fields (time, location, note, etc.)

On save, the AST is updated and DSL is regenerated.


πŸ”„ Two-Way Communication

The Synchronization Model

All three interfaces (DSL editor, Block editor, Canvas) maintain the same source of truth: the DSL string stored in React state.

// PlanPage.tsx - Single source of truth
const [dsl, setDsl] = useState<string>(initialDsl);

// All editors receive and update this state
<DSLEditor value={dsl} onChange={setDsl} />
<WidgetEditor value={dsl} onChange={setDsl} />
<TravelCanvas dsl={dsl} onDslChange={setDsl} />

Update Flow

DSL Editor β†’ Canvas

  1. User types in DSL editor
  2. onChange fires with new DSL string
  3. setDsl(newDsl) updates state
  4. Canvas useEffect triggers (depends on dsl)
  5. Canvas parses DSL and rebuilds nodes

πŸ€– AI Integration

Nami uses Google Gemini 2.5/3.0 Flash for intelligent travel suggestions.

AI Features

  1. Day-specific planning: Click the AI button on any day card
  2. Full trip generation: Use the chat sidebar to plan from scratch
  3. Context-aware suggestions: AI sees your current itinerary
  4. DSL generation: AI outputs valid DSL that can be inserted directly

AI System Prompt

The AI is given:

  • Full DSL syntax specification
  • Current itinerary DSL (for editing suggestions)
  • Destination context
  • User preferences (budget, travel style, interests)
const systemPrompt = `
You are Nami, a travel planning AI. You output itineraries in DSL format.

CURRENT ITINERARY:
${currentDsl}

When the user asks to modify Day 3, output the COMPLETE rewritten day block.
NEVER append duplicatesβ€”always replace the full day.
`;

AI Output Parsing

AI responses are parsed for DSL code blocks:

const extractDsl = (aiResponse: string): string | undefined => {
  const match = aiResponse.match(/```dsl\n([\s\S]*?)```/);
  return match?.[1]?.trim();
};

Users see:

  • Visual preview of the AI's suggestion (rendered as mini day cards)
  • Raw DSL in a code tab
  • Reasoning explanation in a separate tab
  • Insert button to merge into itinerary

πŸ—οΈ Technical Stack

Core Technologies

  • Next.js 16.2 β€” React framework with App Router
  • React 19 β€” UI library
  • TypeScript 5.7 β€” Type safety
  • React Flow 12.3 β€” Canvas and node system
  • Framer Motion 12.0 β€” Animations
  • Tailwind CSS 4.0 β€” Utility-first styling

Key Libraries

  • Zustand β€” Lightweight state management
  • React Markdown β€” Markdown rendering in chat
  • React Joyride β€” Guided tour system
  • Lucide React β€” Icon system

AI & APIs

  • Google Gemini API β€” 2.5 Flash for suggestions
  • OpenAI API β€” (Optional) Alternative LLM backend

πŸ“‚ Project Structure

nami/
β”œβ”€β”€ src/
β”‚   β”œβ”€β”€ app/                    # Next.js App Router
β”‚   β”‚   β”œβ”€β”€ [plan]/             # Dynamic route for plan pages
β”‚   β”‚   β”‚   β”œβ”€β”€ page.tsx        # Plan view page
β”‚   β”‚   β”‚   └── not-found.tsx   # 404 for invalid plans
β”‚   β”‚   └── page.tsx            # Landing page
β”‚   β”‚
β”‚   β”œβ”€β”€ components/
β”‚   β”‚   β”œβ”€β”€ PlanPage.tsx        # Main planning interface
β”‚   β”‚   β”œβ”€β”€ TravelCanvas.tsx    # React Flow canvas
β”‚   β”‚   β”œβ”€β”€ DSLEditor.tsx       # Code editor with syntax highlighting
β”‚   β”‚   β”œβ”€β”€ WidgetEditor.tsx    # Block-style editor
β”‚   β”‚   β”œβ”€β”€ ChatPanel.tsx       # AI chat interface
β”‚   β”‚   β”œβ”€β”€ TopNavBar.tsx       # Top navigation and modals
β”‚   β”‚   β”œβ”€β”€ GuidedTour.tsx      # Onboarding tour
β”‚   β”‚   β”‚
β”‚   β”‚   β”œβ”€β”€ nodes/              # React Flow node components
β”‚   β”‚   β”‚   β”œβ”€β”€ DayNode.tsx     # Day card (container)
β”‚   β”‚   β”‚   β”œβ”€β”€ ActivityNode.tsx # Individual activity
β”‚   β”‚   β”‚   β”œβ”€β”€ RouteNode.tsx   # Route swim lane
β”‚   β”‚   β”‚   β”œβ”€β”€ RouteGroupNode.tsx # Route visual wrapper
β”‚   β”‚   β”‚   └── RouteSectionNode.tsx # Route section divider
β”‚   β”‚   β”‚
β”‚   β”‚   └── edges/
β”‚   β”‚       └── RopeEdge.tsx    # Decorative rope connecting days
β”‚   β”‚
β”‚   └── lib/
β”‚       β”œβ”€β”€ dsl-parser.ts       # DSL β†’ AST parser
β”‚       └── storage.ts          # LocalStorage utilities
β”‚
β”œβ”€β”€ public/
β”‚   β”œβ”€β”€ nami_logo.png           # Logo
β”‚   β”œβ”€β”€ shaun.jpg               # About modal image
β”‚   └── about-music.mp3         # Easter egg audio
β”‚
└── styles/
    └── globals.css             # Global styles and theme

🎨 Design Philosophy

Visual Language

Nami's design is inspired by:

  • Early 2000s web aesthetics (Geocities, MySpace)
  • Warm, earthy tones (travel journal vibes)
  • Clear information hierarchy
  • Playful but functional

Color System

/* Day cards */
--day-bg: #f7f3ec;
--day-border: #c8bda8;
--day-accent: #e8a050;

/* Activity types */
--travel: #5a9ec0;    /* Blue */
--eat: #d48030;       /* Orange */
--activity: #5aaa68;  /* Green */
--stay: #8a6ab8;      /* Purple */
--note: #b8a898;      /* Gray */
--route: #a08060;     /* Brown */

Typography

  • Headings: System font stack (San Francisco, Segoe UI)
  • Code: SF Mono, Fira Code, Consolas
  • Body: -apple-system fallback chain

πŸ“¬ Contact

Shaun Lee

  • Built with vibes in 2026

Happy Planning! βœˆοΈπŸ—ΊοΈ

About

Write Custom Syntax to create travel plans visualised by interactive widgets. Inspired by N8N and Mermaid Diagrams. And yes, theres Ai integration.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors