diff --git a/kits/agentic/smart-travel-planner/.env.example b/kits/agentic/smart-travel-planner/.env.example new file mode 100644 index 00000000..5f035028 --- /dev/null +++ b/kits/agentic/smart-travel-planner/.env.example @@ -0,0 +1,9 @@ +LAMATIC_API_URL=your_lamatic_api_url +LAMATIC_PROJECT_ID=your_project_id +GLOBAL_TRAVEL_AGENT=your_agent_id +LAMATIC_API_KEY=your_api_key +LAMATIC_FLOW_URL=your_flow_url +CHATBOT_FLOW_ID=your_chatbot_flow_id + +GOOGLE_PLACES_API_KEY=your_google_places_api_key +NEXT_PUBLIC_GOOGLE_PLACES_API_KEY=your_google_places_api_key diff --git a/kits/agentic/smart-travel-planner/.gitignore b/kits/agentic/smart-travel-planner/.gitignore new file mode 100644 index 00000000..aaa7918d --- /dev/null +++ b/kits/agentic/smart-travel-planner/.gitignore @@ -0,0 +1,43 @@ +# dependencies +/node_modules + +# next +/.next +/out +/build + +# env +.env.local +.env + +# logs +*.log + +# OS +.DS_Store +Thumbs.db + + +# curl -X POST https://sujalsorganization722-sujalsproject384.lamatic.dev/graphql \ +# -H "Authorization: Bearer lt-8264c32200a2522f111a37a4fb7b252c" \ +# -H "Content-Type: application/json" \ +# -H "x-project-id: a645c6d6-58b3-459b-af94-7c9062de9f2f" \ +# -d '{ +# "query": "query Execute($workflowId: String!, $destination: String!, $days: Int!, $budget: Int!, $destination_type: String!) { executeWorkflow(workflowId: $workflowId, payload: { destination: $destination, days: $days, budget: $budget, destination_type: $destination_type }) { status result } }", +# "variables": { +# "workflowId": "a536ce2e-3b77-482f-9980-523830dffdc5", +# "destination": "Bali", +# "days": 3, +# "budget": 1000, +# "destination_type": "beach" +# } +# }' + + + +# curl -X POST https://edge.lamatic.ai/flow/b1c8e567-44e8-4b16-ad3b-38910d493bb3 \ +# -H "Authorization: Bearer lt-8264c32200a2522f111a37a4fb7b252c" \ +# -H "Content-Type: application/json" \ +# -d '{ +# "message": "What are the best places to visit in Bali?" +# }' diff --git a/kits/agentic/smart-travel-planner/README.md b/kits/agentic/smart-travel-planner/README.md new file mode 100644 index 00000000..22446988 --- /dev/null +++ b/kits/agentic/smart-travel-planner/README.md @@ -0,0 +1,317 @@ +# Smart Travel Planner + +## Live Demo + +https://ai-travel-one-black.vercel.app + +> An **Agentic AI-powered** travel planning platform built with **Next.js** and **Lamatic AI** that generates complete, structured travel itineraries with maps, photos, local food, cultural highlights, and an interactive chatbot. + +--- + +## Features + +- **AI-Generated Itineraries** — Day-wise structured travel plans powered by Lamatic AI LLM flows +- **Live Maps** — OpenStreetMap embedded per day via Geocode API +- **Destination & Day Photos** — Auto-fetched via Places API with lightbox viewer +- **Local Food & Stay Suggestions** — Curated per day and per destination +- **Cultural Highlights & Travel Tips** — Structured output from the LLM +- **Floating Travel Chatbot** — Ask follow-up travel questions in real time +- **Fallback Flow** — Condition-based routing ensures users always get a response +- **GraphQL Workflow Execution** — Lamatic AI called via GraphQL `executeWorkflow` query + +--- + +## Tech Stack + +| Layer | Technology | +|-------|------------| +| Frontend | Next.js 14+ (App Router, TypeScript) | +| Styling | CSS Modules / globals.css | +| AI Orchestration | Lamatic AI (Agentic Workflows via GraphQL) | +| Maps | OpenStreetMap (iframe embed) | +| Location | Geocode API (lat/lng resolution) | +| Photos | Google Places API | +| Language | TypeScript (TSX) | +| Runtime | Node.js | + +--- + +## Project Structure + +``` +smart-travel/ +├── actions/ +│ └── orchestrate.ts # Lamatic AI workflow runners (GraphQL calls) +├── app/ +│ ├── api/ +│ │ ├── chatbot/ +│ │ │ └── route.ts # POST /api/chatbot → runs chatbot flow +│ │ ├── geocode/ +│ │ │ └── route.ts # GET /api/geocode?query= → lat/lng +│ │ ├── map-test/ +│ │ │ └── route.ts # Map sandbox/testing endpoint +│ │ ├── places/ +│ │ │ └── route.ts # GET /api/places?query= → photos[] +│ │ └── travel/ +│ │ └── route.ts # POST /api/travel → full itinerary +│ ├── map-test/ +│ │ └── page.tsx # Map test/debug page +│ ├── globals.css # Global styles +│ ├── layout.tsx # Root layout +│ └── page.tsx # Home page +├── components/ +│ ├── PlannerForm.tsx # Main form + full itinerary UI +│ ├── FloatingChatbot.tsx # Floating AI chat widget +│ ├── ItineraryDisplay.tsx # Itinerary display component +│ └── DayCard.tsx # Single day card component +├── lib/ # Shared utilities +├── public/ # Static assets +├── .env.local # Environment variables (not committed) +└── .gitignore +``` + +--- + +## Lamatic AI Agent Flows + +### Travel Itinerary Flow + +``` +API Request → Code Node → LLM (Generate Itinerary) + │ + Condition Node (validate output) + ┌───────┴────────┐ + Valid Invalid + Refine Text Fallback Text + └───────┬────────┘ + code2 Node (parse JSON) + │ + API Response +``` + +| Node | Purpose | +|------|---------| +| **API Request** | Receives `destination`, `days`, `budget`, `destination_type` | +| **Code Node** | Pre-processes and formats input for the LLM | +| **Generate Itinerary (LLM)** | Core AI node — generates full structured travel plan | +| **Condition Node** | Validates output completeness and structure | +| **Refine Generate Text** | Cleans valid output into structured JSON | +| **Fallback Generate Text** | Generates a simpler plan if the main LLM fails | +| **code2 Node** | Final JSON parse and validation | +| **API Response** | Returns structured itinerary to the frontend | + +### Chatbot Flow + +``` +API Request (user message) → Generate Text (LLM) → API Response +``` + +The chatbot is scoped to travel-only questions: destinations, food, budget, culture, visas, and travel tips. + +--- + +## Lamatic Workflow Integration + +All Lamatic AI flows are called through a shared GraphQL utility in `actions/orchestrate.ts`: + +```typescript +// Generic workflow runner +export async function runLamaticWorkflow({ workflowId, payload }) { + const res = await fetch(process.env.LAMATIC_API_URL, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${process.env.LAMATIC_API_KEY}`, + "x-project-id": process.env.LAMATIC_PROJECT_ID, + }, + body: JSON.stringify({ + query: ` + query Execute($workflowId: String!, $payload: JSON!) { + executeWorkflow(workflowId: $workflowId, payload: $payload) { + status + result + } + } + `, + variables: { workflowId, payload }, + }), + cache: "no-store", + }); +} + +// Travel agent — uses GLOBAL_TRAVEL_AGENT workflow +export async function runTravelAgent(input: { + destination: string; + days: number; + budget: number; + destination_type: string; +}) { ... } + +// Chatbot agent — uses CHATBOT_FLOW_ID workflow +export async function runChatAgent({ message }: { message: string }) { ... } +``` + +--- + +## Key Component — `PlannerForm.tsx` + +The main component handles the complete UI lifecycle: + +**State managed:** +```typescript +destination, days, budget, destinationType // Form inputs +loading, response // Request state +activeDay // Selected day tab +photos // Destination photo gallery +lightboxImg // Lightbox fullscreen image +dayLocation: { lat, lng } // Map coordinates per day +dayPhotos // Per-day photo gallery +``` + +**Data flow on submit:** +``` +handleSubmit() + → POST /api/travel (generate itinerary) + → GET /api/places (fetch destination photos) + → fetchDayLocation() (GET /api/geocode for Day 1 map) + → fetchDayPhoto() (GET /api/places for Day 1 photos) +``` + +**On day tab switch:** +``` +onClick (Day tab) + → fetchDayLocation(morning_location || afternoon_location || destination) + → fetchDayPhoto(location) +``` + +**Map rendering** uses OpenStreetMap via iframe with dynamic bbox: +```typescript +src={`https://www.openstreetmap.org/export/embed.html + ?bbox=${lng - 0.05},${lat - 0.05},${lng + 0.05},${lat + 0.05} + &layer=mapnik&marker=${lat},${lng}`} +``` + +--- + +## API Endpoints + +| Method | Endpoint | Description | +|--------|----------|-------------| +| `POST` | `/api/travel` | Generate full travel itinerary | +| `POST` | `/api/chatbot` | Send message to travel chatbot | +| `GET` | `/api/places?query=` | Fetch destination/place photos | +| `GET` | `/api/geocode?query=` | Resolve place name to lat/lng | + +### Example — Generate Itinerary + +**Request:** +```json +POST /api/travel +{ + "destination": "Bali", + "days": 5, + "budget": 1000, + "destination_type": "beach" +} +``` + +**Response shape:** +```typescript +{ + success: boolean; + itinerary: { + destination: string; + country: string; + introduction: string; + best_time_to_visit: string; + estimated_budget: string; + highlights: { name: string; description: string }[]; + food: { name: string; description: string }[]; + culture: string[]; + travel_tips: string[]; + days: { + day: number; + title: string; + morning: string; + afternoon: string; + evening: string; + morning_location?: string; + afternoon_location?: string; + evening_location?: string; + food_recommendation: string; + stay_suggestion: string; + estimated_day_cost: string; + notes: string; + }[]; + } +} +``` + +--- + +## Getting Started + +### Prerequisites + +- Node.js v18+ +- npm or yarn +- Lamatic AI account with configured workflows +- Google Places API key +- Geocoding API key + +### Installation + +```bash +git clone https://github.com/your-username/smart-travel.git +cd smart-travel +npm install +``` + +### Environment Variables + +Create a `.env.local` file: + +```env +# Lamatic AI +LAMATIC_API_URL=https://your-lamatic-endpoint/graphql +LAMATIC_API_KEY=your_lamatic_api_key +LAMATIC_PROJECT_ID=your_project_id +GLOBAL_TRAVEL_AGENT=your_travel_workflow_id +CHATBOT_FLOW_ID=your_chatbot_workflow_id + +# APIs +GOOGLE_PLACES_API_KEY=your_google_places_api_key +GEOCODE_API_KEY=your_geocode_api_key +``` + +### Run Development Server + +```bash +npm run dev +``` + +Open [http://localhost:3000](http://localhost:3000) + +--- + +## Built With Lamatic AI + +This project was built for the **Lamatic AI Agentic Kit Challenge**. It uses Lamatic's agentic flow infrastructure with multi-node pipelines, condition-based routing, structured JSON outputs, and fallback handling — all connected to a Next.js frontend. + +--- + +## Acknowledgements + +- [Lamatic AI](https://lamatic.ai) — Agentic workflow platform +- [Next.js](https://nextjs.org) — Frontend framework +- [OpenStreetMap](https://openstreetmap.org) — Embedded maps +- Google Places & Geocode APIs — Photos and location resolution + +--- + +## License + +MIT License — feel free to use and adapt this project. + +--- + diff --git a/kits/agentic/smart-travel-planner/actions/orchestrate.ts b/kits/agentic/smart-travel-planner/actions/orchestrate.ts new file mode 100644 index 00000000..1bef4d3b --- /dev/null +++ b/kits/agentic/smart-travel-planner/actions/orchestrate.ts @@ -0,0 +1,117 @@ + +type LamaticResponse = { + data?: { + executeWorkflow?: { + status?: string; + result?: any; + }; + }; + errors?: { message: string }[]; +}; + + + +export async function runLamaticWorkflow({ + workflowId, + payload, +}: { + workflowId: string; + payload: Record; +}) { + const apiUrl = process.env.LAMATIC_API_URL; + const apiKey = process.env.LAMATIC_API_KEY; + const projectId = process.env.LAMATIC_PROJECT_ID; + + if (!apiUrl || !apiKey || !projectId || !workflowId) { + throw new Error("Missing Lamatic environment variables"); + } + + const res = await fetch(apiUrl, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${apiKey}`, + "x-project-id": projectId, + }, + body: JSON.stringify({ + query: ` + query Execute($workflowId: String!, $payload: JSON!) { + executeWorkflow( + workflowId: $workflowId + payload: $payload + ) { + status + result + } + } + `, + variables: { + workflowId, + payload, + }, + }), + cache: "no-store", + }); + + const rawText = await res.text(); + + if (!rawText || !rawText.trim()) { + throw new Error("Lamatic returned an empty response"); + } + + let data: LamaticResponse; + + try { + data = JSON.parse(rawText); + } catch { + throw new Error(`Lamatic returned invalid JSON: ${rawText}`); + } + + if (!res.ok) { + throw new Error( + data?.errors?.[0]?.message || `Lamatic request failed: ${res.status}` + ); + } + + if (data?.errors?.length) { + throw new Error(data.errors[0].message || "Lamatic GraphQL error"); + } + + return data?.data?.executeWorkflow?.result; +} + + +export async function runTravelAgent(input: { + destination: string; + days: number; + budget: number; + destination_type: string; +}) { + const workflowId = process.env.GLOBAL_TRAVEL_AGENT; + + if (!workflowId) { + throw new Error("Missing GLOBAL_TRAVEL_AGENT workflow ID"); + } + + return runLamaticWorkflow({ + workflowId, + payload: input, + }); +} + + + +export async function runChatAgent({ message }: { message: string }) { + const workflowId = process.env.CHATBOT_FLOW_ID; + + if (!workflowId) { + throw new Error("Missing LAMATIC_CHATBOT_FLOW_ID"); + } + + return runLamaticWorkflow({ + workflowId, + payload: { + message, + }, + }); +} \ No newline at end of file diff --git a/kits/agentic/smart-travel-planner/app/api/chatbot/route.ts b/kits/agentic/smart-travel-planner/app/api/chatbot/route.ts new file mode 100644 index 00000000..5bba7c2b --- /dev/null +++ b/kits/agentic/smart-travel-planner/app/api/chatbot/route.ts @@ -0,0 +1,35 @@ +import { NextResponse } from "next/server"; +import { runChatAgent } from "../../../actions/orchestrate"; + +export async function POST(req: Request) { + try { + const body = await req.json(); + const message = String(body?.message || "").trim(); + + if (!message) { + return NextResponse.json( + { success: false, error: "Message is required" }, + { status: 400 } + ); + } + const result = await runChatAgent({ message }); + + return NextResponse.json({ + success: true, + reply: result?.result ?? result, + }); + } catch (error) { + console.error("Chatbot route error:", error); + + return NextResponse.json( + { + success: false, + error: + error instanceof Error + ? error.message + : "Failed to get chatbot reply", + }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/kits/agentic/smart-travel-planner/app/api/geocode/route.ts b/kits/agentic/smart-travel-planner/app/api/geocode/route.ts new file mode 100644 index 00000000..34cb92b5 --- /dev/null +++ b/kits/agentic/smart-travel-planner/app/api/geocode/route.ts @@ -0,0 +1,38 @@ +import { NextRequest, NextResponse } from "next/server"; + +export async function GET(req: NextRequest) { + const { searchParams } = new URL(req.url); + const query = searchParams.get("query"); + + if (!query) { + return NextResponse.json({ error: "No query" }, { status: 400 }); + } + + try { + const res = await fetch( + `https://nominatim.openstreetmap.org/search?q=${encodeURIComponent(query)}&format=json&limit=1`, + { + headers: { + "User-Agent": "SmartTravelPlanner/1.0", + }, + } + ); + + const data = await res.json(); + + if (!data || data.length === 0) { + return NextResponse.json({ lat: null, lng: null }); + } + + return NextResponse.json({ + lat: parseFloat(data[0].lat), + lng: parseFloat(data[0].lon), + }); + + } catch (error) { + return NextResponse.json( + { error: "Geocode failed" }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/kits/agentic/smart-travel-planner/app/api/map-test/route.ts b/kits/agentic/smart-travel-planner/app/api/map-test/route.ts new file mode 100644 index 00000000..cd6b75b0 --- /dev/null +++ b/kits/agentic/smart-travel-planner/app/api/map-test/route.ts @@ -0,0 +1,9 @@ +import { NextResponse } from "next/server"; + +export async function GET() { + return NextResponse.json({ + lat: 27.3314, + lng: 88.6138, + place: "Gangtok" + }); +} \ No newline at end of file diff --git a/kits/agentic/smart-travel-planner/app/api/places/route.ts b/kits/agentic/smart-travel-planner/app/api/places/route.ts new file mode 100644 index 00000000..578c1d42 --- /dev/null +++ b/kits/agentic/smart-travel-planner/app/api/places/route.ts @@ -0,0 +1,37 @@ +import { NextRequest, NextResponse } from "next/server"; + +export async function GET(req: NextRequest) { + const { searchParams } = new URL(req.url); + const query = searchParams.get("query"); + + if (!query) return NextResponse.json({ error: "No query" }, { status: 400 }); + + const apiKey = process.env.GOOGLE_PLACES_API_KEY; + + const searchRes = await fetch( + `https://maps.googleapis.com/maps/api/place/findplacefromtext/json?input=${encodeURIComponent(query)}&inputtype=textquery&fields=place_id,name&key=${apiKey}` + ); + const searchData = await searchRes.json(); + const placeId = searchData?.candidates?.[0]?.place_id; + + if (!placeId) return NextResponse.json({ photos: [] }); + + + const detailsRes = await fetch( + `https://maps.googleapis.com/maps/api/place/details/json?place_id=${placeId}&fields=photos,name,geometry&key=${apiKey}` + ); + const detailsData = await detailsRes.json(); + const photos = detailsData?.result?.photos?.slice(0, 6) ?? []; + const location = detailsData?.result?.geometry?.location ?? null; + + + const photoUrls = photos.map((p: { photo_reference: string }) => + `https://maps.googleapis.com/maps/api/place/photo?maxwidth=800&photo_reference=${p.photo_reference}&key=${apiKey}` + ); + + return NextResponse.json({ + photos: photoUrls, + name: detailsData?.result?.name, + location, + }); +} \ No newline at end of file diff --git a/kits/agentic/smart-travel-planner/app/api/travel/route.ts b/kits/agentic/smart-travel-planner/app/api/travel/route.ts new file mode 100644 index 00000000..99b8d5d6 --- /dev/null +++ b/kits/agentic/smart-travel-planner/app/api/travel/route.ts @@ -0,0 +1,53 @@ +import { NextResponse } from "next/server"; +import { runTravelAgent } from "../../../actions/orchestrate"; + +export async function POST(req: Request) { + try { + const body = await req.json(); + + const destination = String(body?.destination || "").trim(); + const days = Number(body?.days); + const budget = Number(body?.budget); + const destination_type = String(body?.destination_type || "").trim(); + + if (!destination || !days || !budget) { + return NextResponse.json( + { + success: false, + error: "destination, days, budget and destination_type are required", + }, + { status: 400 } + ); + } + + const agentResponse = await runTravelAgent({ + destination, + days, + budget, + destination_type, + }); + + const finalData = + agentResponse?.result?.result || + agentResponse?.result || + agentResponse; + + return NextResponse.json({ + success: true, + itinerary: finalData, + }); + } catch (error) { + console.error("Travel route error:", error); + + return NextResponse.json( + { + success: false, + error: + error instanceof Error + ? error.message + : "Failed to generate travel itinerary", + }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/kits/agentic/smart-travel-planner/app/globals.css b/kits/agentic/smart-travel-planner/app/globals.css new file mode 100644 index 00000000..dfe90bb8 --- /dev/null +++ b/kits/agentic/smart-travel-planner/app/globals.css @@ -0,0 +1,738 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +html, +body { + font-family: Arial, Helvetica, sans-serif; + background: #061425; + color: #ffffff; +} + +body { + min-height: 100vh; +} + +.home-page { + min-height: 100vh; + background: + radial-gradient(circle at top left, rgba(0, 153, 255, 0.15), transparent 30%), + radial-gradient(circle at bottom right, rgba(0, 255, 170, 0.12), transparent 25%), + linear-gradient(135deg, #04111f, #071a2f 50%, #03101d); + padding: 60px 20px 100px; +} + +.hero-section { + max-width: 1100px; + margin: 0 auto; + text-align: center; +} + +.hero-badge { + display: inline-block; + padding: 10px 18px; + border: 1px solid rgba(255, 255, 255, 0.18); + border-radius: 999px; + background: rgba(255, 255, 255, 0.08); + font-size: 14px; + margin-bottom: 22px; + backdrop-filter: blur(8px); +} + +.hero-title { + font-size: 54px; + line-height: 1.1; + font-weight: 800; + margin-bottom: 18px; +} + +.hero-subtitle { + max-width: 760px; + margin: 0 auto 36px; + color: rgba(255, 255, 255, 0.78); + font-size: 18px; + line-height: 1.7; +} + +/* ── Planner card ── */ +.planner-card { + max-width: 980px; + margin: 0 auto; + display: flex; + flex-direction: column; + gap: 16px; +} + +.planner-form { + background: rgba(255, 255, 255, 0.07); + border: 1px solid rgba(255, 255, 255, 0.13); + border-radius: 20px; + padding: 26px; + backdrop-filter: blur(12px); + display: flex; + flex-direction: column; + gap: 20px; +} + +.form-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 16px; +} + +.input-group { + display: flex; + flex-direction: column; + text-align: left; + gap: 6px; +} + +.input-group label { + font-size: 11px; + text-transform: uppercase; + letter-spacing: 0.08em; + color: rgba(255, 255, 255, 0.4); + font-weight: 500; +} + +.input-group input, +.input-group select { + width: 100%; + border: 0.5px solid rgba(255, 255, 255, 0.12); + background: rgba(255, 255, 255, 0.05); + color: white; + padding: 10px 13px; + border-radius: 10px; + outline: none; + font-size: 14px; + font-family: inherit; +} + +.input-group input::placeholder { + color: rgba(255, 255, 255, 0.25); +} + +.input-group input:focus, +.input-group select:focus { + border-color: rgba(255, 255, 255, 0.3); +} + +.input-group select option { + background: #0a1930; +} + +.primary-btn { + border: none; + background: #3b82f6; + color: white; + padding: 13px 22px; + border-radius: 12px; + font-size: 15px; + font-weight: 600; + cursor: pointer; + transition: background 0.2s, transform 0.15s; + display: flex; + align-items: center; + justify-content: center; + gap: 8px; +} + +.primary-btn:hover { + background: #60a5fa; +} + +.primary-btn:disabled { + opacity: 0.6; + cursor: not-allowed; + transform: none; +} + +/* ── Error ── */ +.error-text { + background: rgba(239, 68, 68, 0.1); + border: 0.5px solid rgba(239, 68, 68, 0.25); + border-radius: 14px; + padding: 16px; + color: #fca5a5; + font-size: 14px; +} + +/* ── Result output ── */ +.result-card { + background: rgba(255, 255, 255, 0.06); + border: 0.5px solid rgba(255, 255, 255, 0.11); + border-radius: 20px; + padding: 22px; + display: flex; + flex-direction: column; + gap: 14px; +} + +.result-card h3 { + font-size: 11px; + text-transform: uppercase; + letter-spacing: 0.1em; + color: rgba(255, 255, 255, 0.35); + font-weight: 500; +} + +/* Hero block */ +.itinerary-hero { + display: flex; + flex-direction: column; + gap: 10px; +} + +.itinerary-hero h2 { + font-size: 26px; + font-weight: 700; + color: #fff; +} + +.itinerary-hero p { + font-size: 13px; + color: rgba(255, 255, 255, 0.55); + line-height: 1.65; +} + +.stat-pills { + display: flex; + flex-wrap: wrap; + gap: 10px; + margin-top: 4px; +} + +.stat-pill { + background: rgba(255, 255, 255, 0.06); + border: 0.5px solid rgba(255, 255, 255, 0.1); + border-radius: 10px; + padding: 9px 14px; +} + +.stat-pill .pill-label { + font-size: 10px; + text-transform: uppercase; + letter-spacing: 0.08em; + color: rgba(255, 255, 255, 0.32); + margin-bottom: 2px; +} + +.stat-pill .pill-value { + font-size: 13px; + color: rgba(255, 255, 255, 0.78); +} + +/* Two-column grid for highlight/food/culture/tips */ +.info-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 14px; +} + +.info-section { + background: rgba(255, 255, 255, 0.05); + border: 0.5px solid rgba(255, 255, 255, 0.09); + border-radius: 14px; + padding: 16px; +} + +.info-section h4 { + font-size: 10px; + text-transform: uppercase; + letter-spacing: 0.1em; + color: rgba(255, 255, 255, 0.35); + margin-bottom: 12px; +} + +/* Highlight items */ +.info-block { + border-left: 2px solid rgba(59, 130, 246, 0.5); + padding-left: 10px; + margin-bottom: 10px; + border-radius: 0; +} + +.info-block.food-block { + border-left-color: rgba(251, 146, 60, 0.55); +} + +.info-block strong { + display: block; + font-size: 13px; + color: rgba(255, 255, 255, 0.88); + margin-bottom: 2px; +} + +.info-block p { + font-size: 12px; + color: rgba(255, 255, 255, 0.42); + line-height: 1.5; +} + +/* Bullet lists */ +.bullet-list { + list-style: none; + display: flex; + flex-direction: column; + gap: 7px; +} + +.bullet-list li { + display: flex; + gap: 8px; + font-size: 13px; + color: rgba(255, 255, 255, 0.58); + line-height: 1.5; +} + +.bullet-list li::before { + content: ""; + width: 6px; + height: 6px; + border-radius: 50%; + flex-shrink: 0; + margin-top: 5px; +} + +.bullet-list.culture li::before { + background: #a78bfa; +} + +.bullet-list.tips li::before { + background: #4ade80; +} + +/* Day-wise plan */ +.day-plan-section { + background: rgba(255, 255, 255, 0.05); + border: 0.5px solid rgba(255, 255, 255, 0.09); + border-radius: 14px; + padding: 18px; +} + +.day-plan-section h4 { + font-size: 10px; + text-transform: uppercase; + letter-spacing: 0.1em; + color: rgba(255, 255, 255, 0.35); + margin-bottom: 14px; +} + +.day-tabs { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-bottom: 18px; +} + +.day-tab { + padding: 5px 14px; + border-radius: 999px; + font-size: 12px; + font-weight: 500; + border: 0.5px solid rgba(255, 255, 255, 0.12); + background: rgba(255, 255, 255, 0.04); + color: rgba(255, 255, 255, 0.45); + cursor: pointer; + transition: all 0.15s; + font-family: inherit; +} + +.day-tab.active { + background: #3b82f6; + border-color: #3b82f6; + color: #fff; +} + +.day-tab:hover:not(.active) { + color: rgba(255, 255, 255, 0.8); + border-color: rgba(255, 255, 255, 0.25); +} + +.day-title { + font-size: 17px; + font-weight: 600; + color: #fff; + margin-bottom: 14px; +} + +.time-grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 10px; + margin-bottom: 10px; +} + +.time-card { + background: rgba(255, 255, 255, 0.04); + border-radius: 10px; + padding: 11px 12px; + border-left: 2px solid transparent; + border-radius: 0 10px 10px 0; +} + +.time-card.morning { + border-left-color: rgba(234, 179, 8, 0.6); +} + +.time-card.afternoon { + border-left-color: rgba(251, 146, 60, 0.6); +} + +.time-card.evening { + border-left-color: rgba(167, 139, 250, 0.6); +} + +.time-card .tc-label { + font-size: 10px; + text-transform: uppercase; + letter-spacing: 0.08em; + color: rgba(255, 255, 255, 0.3); + margin-bottom: 4px; +} + +.time-card .tc-value { + font-size: 12px; + color: rgba(255, 255, 255, 0.7); + line-height: 1.55; +} + +.meta-grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 10px; + margin-bottom: 10px; +} + +.meta-card { + background: rgba(255, 255, 255, 0.04); + border-radius: 10px; + padding: 10px 12px; +} + +.meta-card .mc-label { + font-size: 10px; + text-transform: uppercase; + letter-spacing: 0.08em; + color: rgba(255, 255, 255, 0.3); + margin-bottom: 3px; +} + +.meta-card .mc-value { + font-size: 12px; + color: rgba(255, 255, 255, 0.65); +} + +.meta-card.cost .mc-value { + color: rgba(255, 255, 255, 0.88); + font-weight: 600; +} + +.notes-box { + background: rgba(234, 179, 8, 0.06); + border: 0.5px solid rgba(234, 179, 8, 0.18); + border-radius: 10px; + padding: 10px 13px; +} + +.notes-box .notes-label { + font-size: 10px; + text-transform: uppercase; + letter-spacing: 0.08em; + color: rgba(234, 179, 8, 0.55); + margin-bottom: 3px; +} + +.notes-box .notes-value { + font-size: 12px; + color: rgba(255, 255, 255, 0.55); +} + +/* ── Chatbot ── */ +.chat-fab { + position: fixed; + right: 24px; + bottom: 24px; + width: 62px; + height: 62px; + border-radius: 50%; + border: none; + background: linear-gradient(135deg, #00c6ff, #0072ff); + color: white; + font-size: 26px; + cursor: pointer; + box-shadow: 0 12px 30px rgba(0, 114, 255, 0.35); + z-index: 999; +} + +.chat-window { + position: fixed; + right: 24px; + bottom: 98px; + width: 360px; + height: 520px; + background: #0b1c2f; + border: 1px solid rgba(255, 255, 255, 0.12); + border-radius: 22px; + overflow: hidden; + display: flex; + flex-direction: column; + box-shadow: 0 18px 50px rgba(0, 0, 0, 0.35); + z-index: 999; +} + +.chat-header { + padding: 16px 18px; + background: linear-gradient(135deg, #102a43, #163d63); + border-bottom: 1px solid rgba(255, 255, 255, 0.08); + display: flex; + align-items: center; + justify-content: space-between; +} + +.chat-header h3 { + font-size: 18px; + margin-bottom: 2px; +} + +.chat-header p { + font-size: 13px; + color: rgba(255, 255, 255, 0.7); +} + +.chat-close { + background: transparent; + border: none; + color: white; + font-size: 26px; + cursor: pointer; +} + +.chat-messages { + flex: 1; + padding: 16px; + overflow-y: auto; + display: flex; + flex-direction: column; + gap: 12px; + background: rgba(255, 255, 255, 0.02); +} + +.chat-bubble { + max-width: 82%; + padding: 12px 14px; + border-radius: 16px; + font-size: 14px; + line-height: 1.5; +} + +.bot-bubble { + align-self: flex-start; + background: rgba(255, 255, 255, 0.08); + color: white; +} + +.user-bubble { + align-self: flex-end; + background: linear-gradient(135deg, #00c6ff, #0072ff); + color: white; +} + +.chat-input-area { + padding: 14px; + display: flex; + gap: 10px; + border-top: 1px solid rgba(255, 255, 255, 0.08); + background: #0d2238; +} + +.chat-input-area input { + flex: 1; + border: 1px solid rgba(255, 255, 255, 0.12); + background: rgba(255, 255, 255, 0.07); + color: white; + border-radius: 12px; + padding: 12px 14px; + outline: none; +} + +.chat-input-area input::placeholder { + color: rgba(255, 255, 255, 0.45); +} + +.chat-input-area button { + border: none; + background: linear-gradient(135deg, #00c6ff, #0072ff); + color: white; + border-radius: 12px; + padding: 12px 16px; + font-weight: 700; + cursor: pointer; +} + +@media (max-width: 768px) { + .hero-title { font-size: 38px; } + .hero-subtitle { font-size: 16px; } + .form-grid { grid-template-columns: 1fr; } + .info-grid { grid-template-columns: 1fr; } + .time-grid { grid-template-columns: 1fr; } + .meta-grid { grid-template-columns: 1fr 1fr; } + + .chat-window { + right: 12px; + left: 12px; + bottom: 88px; + width: auto; + height: 70vh; + } + + .chat-fab { + right: 16px; + bottom: 16px; + } +} + + +/* ── Photo Gallery ── */ +.gallery-section { + background: rgba(255, 255, 255, 0.05); + border: 0.5px solid rgba(255, 255, 255, 0.09); + border-radius: 14px; + padding: 18px; +} + +.gallery-section h4 { + font-size: 10px; + text-transform: uppercase; + letter-spacing: 0.1em; + color: rgba(255, 255, 255, 0.35); + margin-bottom: 14px; +} + +.photo-grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 10px; +} + +.photo-grid img { + width: 100%; + height: 160px; + object-fit: cover; + border-radius: 10px; + cursor: pointer; + transition: opacity 0.2s, transform 0.2s; + border: 0.5px solid rgba(255, 255, 255, 0.08); +} + +.photo-grid img:hover { + opacity: 0.85; + transform: scale(1.02); +} + +/* Lightbox */ +.lightbox-overlay { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.88); + z-index: 1000; + display: flex; + align-items: center; + justify-content: center; + padding: 24px; +} + +.lightbox-overlay img { + max-width: 90vw; + max-height: 85vh; + border-radius: 14px; + object-fit: contain; +} + +.lightbox-close { + position: absolute; + top: 20px; + right: 24px; + background: transparent; + border: none; + color: white; + font-size: 32px; + cursor: pointer; + line-height: 1; +} + +@media (max-width: 768px) { + .photo-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } + .photo-grid img { height: 120px; } +} + + + +/* ── Day Map ── */ +.day-map-section { + margin-top: 14px; + border-radius: 12px; + overflow: hidden; + border: 0.5px solid rgba(255, 255, 255, 0.09); + height: 280px; + position: relative; +} + +.day-map-section iframe { + width: 100%; + height: 100%; + border: none; + display: block; +} + +.map-loading { + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + background: rgba(255, 255, 255, 0.03); + color: rgba(255, 255, 255, 0.35); + font-size: 13px; +} + + +.day-photo-section { + margin-top: 14px; + border-radius: 12px; + overflow: hidden; + border: 0.5px solid rgba(255, 255, 255, 0.09); + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 6px; + padding: 6px; + background: rgba(255, 255, 255, 0.03); +} + +.day-photo-section img { + width: 100%; + height: 120px; + object-fit: cover; + border-radius: 8px; + cursor: pointer; + transition: opacity 0.2s, transform 0.2s; +} + +.day-photo-section img:hover { + opacity: 0.85; + transform: scale(1.02); +} + +.day-photo-loading { + height: 200px; + display: flex; + align-items: center; + justify-content: center; + background: rgba(255, 255, 255, 0.03); + color: rgba(255, 255, 255, 0.35); + font-size: 13px; + border-radius: 12px; + margin-top: 14px; +} \ No newline at end of file diff --git a/kits/agentic/smart-travel-planner/app/layout.tsx b/kits/agentic/smart-travel-planner/app/layout.tsx new file mode 100644 index 00000000..173e9aad --- /dev/null +++ b/kits/agentic/smart-travel-planner/app/layout.tsx @@ -0,0 +1,19 @@ +import "./globals.css"; +import type { Metadata } from "next"; + +export const metadata: Metadata = { + title: "Smart Travel Planner", + description: "AI-powered travel planner with itinerary generator and travel chatbot", +}; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + {children} + + ); +} \ No newline at end of file diff --git a/kits/agentic/smart-travel-planner/app/map-test/page.tsx b/kits/agentic/smart-travel-planner/app/map-test/page.tsx new file mode 100644 index 00000000..d2ee29f1 --- /dev/null +++ b/kits/agentic/smart-travel-planner/app/map-test/page.tsx @@ -0,0 +1,31 @@ +"use client"; + +import { useEffect, useState } from "react"; + +export default function MapTest() { + const [location, setLocation] = useState(null); + + useEffect(() => { + fetch("/api/map-test") + .then(res => res.json()) + .then(data => { + console.log("Test map data:", data); + setLocation(data); + }); + }, []); + + return ( +
+

Map Test

+ + {location ? ( +