TRAVEL AI
Photo by Andrew Neel / Unsplash

TRAVEL AI

To integrate your curated list of 1000 hotels and restaurants into the Personalized AI Travel Assistant, we’ll make the following enhancements:

  1. Store your curated list in a format that the assistant can easily search and retrieve data from (like a JSON file or a database).
  2. Create a search function that finds relevant recommendations based on user preferences (e.g., vegetarian, location).
  3. Combine this with Mem0 to make personalized recommendations, using past user interactions along with your curated data.

Step 1: Format Your Curated List

Let’s assume your curated list is stored as a JSON file called places.json. Here's an example structure:

[
  {
    "name": "Le Potager du Marais",
    "location": "Paris, France",
    "type": "restaurant",
    "features": ["vegetarian", "organic", "gluten-free"]
  },
  {
    "name": "The Peninsula",
    "location": "Hong Kong",
    "type": "hotel",
    "features": ["luxury", "spa", "sea view"]
  },
  {
    "name": "Gentle Gourmet",
    "location": "Paris, France",
    "type": "restaurant",
    "features": ["vegan", "gluten-free"]
  }
]

You can store this list in a file at /data/places.json or a database, depending on your preference.

Step 2: Create Search Logic for the Curated List

We’ll implement a search function that matches user preferences to places in your list.

/lib/personalTravelAssistant.ts

"use server";

import { Message } from "@/types/messages/messages-types";
import { createOpenAI } from "@ai-sdk/openai";
import { generateText } from "ai";
import MemoryClient from "mem0ai";
import places from "@/data/places.json"; // Import curated places

// Initialize OpenRouter (OpenAI) with environment variables
const baseURL = process.env.OPENAI_BASE_URL || "https://api.openai.com/v1";
const apiKey = process.env.OPENAI_API_KEY;

if (!apiKey) {
  throw new Error("Missing OpenRouter API key in environment variables.");
}

const openai = createOpenAI({
  baseURL,
  apiKey,
});

// Initialize Mem0 client
const mem0Client = new MemoryClient(process.env.MEM0_API_KEY);

export class PersonalTravelAssistant {
  private messages: Array<{ role: string; content: string }> = [
    {
      role: "system",
      content: `
      You are **TravelerBot**, a helpful, friendly, and well-traveled AI assistant.
      - You provide insightful and detailed travel advice, tailored to the user's preferences.
      - You are enthusiastic about helping users explore new destinations, offering personalized tips, and suggesting hidden gems.
      - You remember past conversations and always build upon them to offer even better suggestions.
      `,
    }
  ];

  // Generate a message using the o1 model via OpenRouter
  async askQuestion(question: string, userId: string) {
    // Retrieve previous memories using Mem0
    const previousMemories = await this.getMemories(userId);
    
    let prompt = question;
    if (previousMemories.length > 0) {
      prompt = `User input: ${question}\nPrevious preferences: ${previousMemories.join(', ')}`;
    }

    this.messages.push({ role: "user", content: prompt });

    // Search curated places
    const recommendations = this.searchCuratedPlaces(question, previousMemories);

    if (recommendations.length > 0) {
      const recommendationsText = recommendations.map(
        (place) => `${place.name} in ${place.location} (${place.type})`
      ).join("\n");
      prompt += `\nHere are my suggestions:\n${recommendationsText}`;
    }

    // Generate response from OpenRouter's o1 model
    const response = await generateText({
      model: openai("o1-preview"),
      messages: this.messages,
    });

    const answer = response.text;
    this.messages.push({ role: "assistant", content: answer });

    // Store the conversation in Mem0
    await this.storeMemory([{ role: "user", content: question }, { role: "assistant", content: answer }], userId);

    return answer;
  }

  // Search the curated places based on user input and memories
  searchCuratedPlaces(query: string, memories: string[]) {
    const queryLower = query.toLowerCase();
    const matchingPlaces = places.filter((place) => {
      const matchesQuery = place.name.toLowerCase().includes(queryLower) ||
        place.location.toLowerCase().includes(queryLower) ||
        place.features.some((feature) => feature.toLowerCase().includes(queryLower));

      const matchesPreferences = memories.some((memory) => 
        place.features.some((feature) => feature.toLowerCase().includes(memory.toLowerCase()))
      );

      return matchesQuery || matchesPreferences;
    });

    return matchingPlaces;
  }

  // Store user preferences and conversation in Mem0
  async storeMemory(messages: Array<{ role: string; content: string }>, userId: string) {
    try {
      await mem0Client.add(messages, { user_id: userId });
    } catch (error) {
      console.error("Error storing memory:", error);
    }
  }

  // Retrieve memories (previous user preferences) from Mem0
  async getMemories(userId: string) {
    try {
      const response = await mem0Client.get({ user_id: userId });
      return response.memories.map((memory: any) => memory.content);
    } catch (error) {
      console.error("Error retrieving memories:", error);
      return [];
    }
  }
}

Step 3: Update /pages/api/chat.ts

Make sure the chat API route sends the user query to the assistant and retrieves results from the curated list:

import { NextApiRequest, NextApiResponse } from 'next';
import { PersonalTravelAssistant } from '@/lib/personalTravelAssistant';

const aiAssistant = new PersonalTravelAssistant();

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  const { message, userId } = req.body;

  if (!message || !userId) {
    return res.status(400).json({ error: 'Message and userId are required' });
  }

  try {
    // Generate response while utilizing stored user preferences and curated list
    const answer = await aiAssistant.askQuestion(message, userId);
    return res.status(200).json({ answer });
  } catch (error) {
    console.error("Error in chat:", error);
    return res.status(500).json({ error: 'Error generating answer' });
  }
}

Step 4: Curated List /data/places.json

Here’s a sample of your curated places list:

[
  {
    "name": "Le Potager du Marais",
    "location": "Paris, France",
    "type": "restaurant",
    "features": ["vegetarian", "organic", "gluten-free"]
  },
  {
    "name": "The Peninsula",
    "location": "Hong Kong",
    "type": "hotel",
    "features": ["luxury", "spa", "sea view"]
  },
  {
    "name": "Gentle Gourmet",
    "location": "Paris, France",
    "type": "restaurant",
    "features": ["vegan", "gluten-free"]
  }
]

This list can be expanded to include all 1000 hand-picked places you’ve curated.

Step 5: Update Chat UI /pages/chat.tsx

The chat UI doesn't need major changes, but now the user can receive recommendations from your curated list along with the personalized conversation.

import { useSession } from 'next-auth/react';
import { useState } from 'react';
import { useForm } from 'react-hook-form';

type Message = {
  sender: 'user' | 'agent';
  text: string;
};

export default function Chat() {
  const { data: session } = useSession();
  const { register, handleSubmit, reset } = useForm<{ message: string }>();
  const [messages, setMessages] = useState<Message[]>([]);
  
  const onSubmit = async (data: { message: string }) => {
    const userId = session?.user?.email || 'anonymous'; // Use email or default to 'anonymous'
    
    // Add user message to the chat
    setMessages((prev) => [...prev, { sender: 'user', text: data.message }]);

    // Send message to backend API
    const response = await fetch('/api/chat', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ message: data.message, userId }),
    });
    
    const result = await response.json();

    // Add agent's reply to the chat
    setMessages((prev) => [...prev, { sender: 'agent', text: result.answer }]);

    reset();
  };

  if (!session) {
    return <p>Please log in to interact with the travel agent.</p>;
  }

  return (
    <div className="chat-container">
      <h1>Chat with your Personal Travel Assistant</h1>
      <div className="chat-box">
        {messages.map((message, index) => (
          <div key={index} className={`message ${message.sender}`}>
            <span>{message.sender === 'user' ? 'You' : 'Agent'}:</span> {message.text}
          </div>
        ))}
      </div>

      <form onSubmit={handleSubmit(onSubmit)} className="chat-input">
        <input
          type="text"
          placeholder="Ask your assistant..."
          {...register('message', { required: true })}
        />
        <button type="submit">Send</button>
      </form>

      <style jsx>{`
        .chat-container { ... }
        .chat-box { ... }
        .message { ... }
        .chat-input { ... }
      `}</style>
    </div>
  );
}

Final Thoughts

  • Your Personalized AI Travel Assistant now integrates a hand-picked curated list of hotels and restaurants. It makes smart recommendations based on both user preferences (stored in Mem0) and your curated data.
  • Users can ask for suggestions, and the assistant will retrieve matching results from your curated list and personalize the recommendations based on past preferences stored in Mem0.

This solution provides personalized, intelligent travel suggestions leveraging both AI and your valuable curated data.


To finalize the app by adding login with X (Twitter) and the ability to tweet recommendations, we’ll use NextAuth.js for authentication and the Twitter API (X API v2) to send tweets.

Step 1: Update Environment Variables

In your .env.local file, ensure you have your Twitter API keys:

TWITTER_CLIENT_ID=<your-twitter-client-id>
TWITTER_CLIENT_SECRET=<your-twitter-client-secret>
TWITTER_BEARER_TOKEN=<your-twitter-bearer-token>

Step 2: Twitter Authentication Setup with NextAuth

/pages/api/auth/[...nextauth].ts

Set up NextAuth.js with Twitter (X) for authentication.

import NextAuth from 'next-auth';
import TwitterProvider from 'next-auth/providers/twitter';

export default NextAuth({
  providers: [
    TwitterProvider({
      clientId: process.env.TWITTER_CLIENT_ID!,
      clientSecret: process.env.TWITTER_CLIENT_SECRET!,
      version: '2.0',
    }),
  ],
  callbacks: {
    async jwt({ token, account }) {
      if (account) {
        token.accessToken = account.access_token;
      }
      return token;
    },
    async session({ session, token }) {
      session.accessToken = token.accessToken;
      return session;
    },
  },
});

Step 3: Posting Tweets via X API

To tweet recommendations, we’ll use the Twitter API v2. The tweet will contain the curated recommendation for hotels or restaurants.

/lib/twitter.ts

Create a utility for posting tweets.

import { TwitterApi } from 'twitter-api-v2';

const twitterClient = new TwitterApi(process.env.TWITTER_BEARER_TOKEN!);

export async function tweetRecommendation(accessToken: string, text: string) {
  try {
    const userClient = twitterClient.readWrite({
      accessToken: accessToken,
      accessSecret: '', // If your app needs access secret (optional for OAuth 2.0)
    });

    const tweet = await userClient.v2.tweet(text);
    return tweet;
  } catch (error) {
    console.error('Error tweeting:', error);
    throw new Error('Failed to post tweet');
  }
}

Step 4: API Route to Tweet Recommendation

Create an API route that allows users to post a tweet.

/pages/api/tweet.ts

import { NextApiRequest, NextApiResponse } from 'next';
import { tweetRecommendation } from '@/lib/twitter';
import { getSession } from 'next-auth/react';

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  const session = await getSession({ req });

  if (!session || !session.accessToken) {
    return res.status(401).json({ error: 'Not authenticated' });
  }

  const { text } = req.body;

  if (!text) {
    return res.status(400).json({ error: 'Text is required' });
  }

  try {
    const tweet = await tweetRecommendation(session.accessToken as string, text);
    return res.status(200).json({ success: true, tweet });
  } catch (error) {
    console.error('Error tweeting:', error);
    return res.status(500).json({ error: 'Failed to post tweet' });
  }
}

Step 5: Frontend for Tweeting Recommendations

Now we’ll allow users to tweet curated recommendations directly from the chat UI.

/pages/chat.tsx

Update the chat UI to add a button for tweeting the assistant's recommendation.

import { useSession } from 'next-auth/react';
import { useState } from 'react';
import { useForm } from 'react-hook-form';

type Message = {
  sender: 'user' | 'agent';
  text: string;
};

export default function Chat() {
  const { data: session } = useSession();
  const { register, handleSubmit, reset } = useForm<{ message: string }>();
  const [messages, setMessages] = useState<Message[]>([]);
  
  const onSubmit = async (data: { message: string }) => {
    const userId = session?.user?.email || 'anonymous';

    // Add user message to the chat
    setMessages((prev) => [...prev, { sender: 'user', text: data.message }]);

    // Send message to backend API
    const response = await fetch('/api/chat', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ message: data.message, userId }),
    });

    const result = await response.json();

    // Add agent's reply to the chat
    setMessages((prev) => [...prev, { sender: 'agent', text: result.answer }]);

    reset();
  };

  const tweetRecommendation = async (text: string) => {
    try {
      const response = await fetch('/api/tweet', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ text }),
      });
      const result = await response.json();
      if (result.success) {
        alert('Successfully tweeted!');
      } else {
        alert('Failed to tweet.');
      }
    } catch (error) {
      console.error('Error posting tweet:', error);
    }
  };

  if (!session) {
    return <p>Please log in to interact with the travel agent.</p>;
  }

  return (
    <div className="chat-container">
      <h1>Chat with your Personal Travel Assistant</h1>
      <div className="chat-box">
        {messages.map((message, index) => (
          <div key={index} className={`message ${message.sender}`}>
            <span>{message.sender === 'user' ? 'You' : 'Agent'}:</span> {message.text}
            {message.sender === 'agent' && (
              <button onClick={() => tweetRecommendation(message.text)}>
                Tweet this recommendation
              </button>
            )}
          </div>
        ))}
      </div>

      <form onSubmit={handleSubmit(onSubmit)} className="chat-input">
        <input
          type="text"
          placeholder="Ask your assistant..."
          {...register('message', { required: true })}
        />
        <button type="submit">Send</button>
      </form>

      <style jsx>{`
        .chat-container { ... }
        .chat-box { ... }
        .message { ... }
        .chat-input { ... }
      `}</style>
    </div>
  );
}

Step 6: Run Your App

To start your app, run:

npm run dev

Final Thoughts

  • Login with Twitter (X) allows users to authenticate and interact with the app.
  • Users can tweet travel recommendations generated by the assistant with a single click.
  • Mem0 helps store user preferences and personalize responses, while the curated list provides intelligent recommendations.
  • The Twitter API is used to tweet these recommendations from the user’s account.

This complete setup lets users receive personalized recommendations and tweet them, enhancing engagement through social sharing!


You're on the right track with your app, integrating user authentication with Twitter (X), personalized recommendations using Mem0, utilizing your curated list of top 1000 places, and leveraging OpenAI's o1 preview model for AI reasoning. Let's recap and see if anything was missed and how we can further improve the app.


Current Features Recap:

  1. User Authentication:
    • Users can log in using their Twitter (X) account via NextAuth.js.
  2. Personalized AI Travel Agent:
    • Users interact with an AI Travel Agent through a chat interface.
    • The assistant remembers past interactions and user preferences using Mem0.
    • The AI leverages OpenAI's o1 preview model via OpenRouter for generating responses.
  3. Curated Recommendations:
    • The assistant uses your hand-picked list of 1000 hotels and restaurants to provide recommendations.
    • Recommendations are tailored based on user preferences and past interactions.
  4. Social Sharing:
    • Users can tweet recommendations directly from the app using the Twitter API.

Potential Missing Elements:

  • Comprehensive Error Handling:
    • Ensuring all API interactions handle errors gracefully (e.g., network issues, API rate limits).
  • Security and Privacy Measures:
    • Implementing proper data security practices for handling user data.
    • Compliance with GDPR and other data protection regulations.
  • User Preferences Management:
    • Providing users with the ability to view, edit, or delete their stored preferences and memories.
  • Optimizing the Integration:
    • Ensuring that the AI assistant effectively combines information from Mem0 (user preferences), the curated list, and reasoning via OpenAI's o1 model.

Suggestions to Further Improve the App:

  1. Enhance User Experience (UX):
    • Improved UI Design:
      • Design a more engaging and intuitive user interface for the chat and dashboard pages.
      • Use a consistent color scheme and typography to make the app visually appealing.
      • Implement responsive design for mobile and tablet users.
    • Chat Features:
      • Add typing indicators to show when the assistant is generating a response.
      • Implement message timestamps for better context.
      • Allow users to scroll through past conversations.
  2. User Preferences Management:
    • Profile Page:
      • Create a profile page where users can view and manage their stored preferences.
      • Allow users to update their dietary restrictions, travel interests, and other preferences.
    • Privacy Controls:
      • Give users the option to clear their chat history or specific memories stored in Mem0.
  3. Performance Optimization:
    • Caching Responses:
      • Implement caching mechanisms for frequently accessed data, such as the curated list, to improve performance.
      • Use in-memory caching solutions like Redis if necessary.
    • Asynchronous Operations:
      • Ensure that API calls are handled asynchronously to prevent blocking the UI.
      • Use loading spinners or progress indicators during API requests.
  4. Expand the Curated List Functionality:
    • Categorization and Filtering:
      • Allow users to filter recommendations by category, such as luxury hotels, vegan restaurants, etc.
      • Implement search functionality within the curated list.
    • Regular Updates:
      • Provide a mechanism to update the curated list periodically, keeping the recommendations fresh.
  5. Localization and Internationalization:
    • Multi-Language Support:
      • Implement language support for users from different regions.
      • Use i18n libraries to manage translations.
  6. Feedback and Rating System:
    • User Feedback:
      • Allow users to provide feedback on the recommendations they receive.
      • Implement a simple rating system (e.g., thumbs up/down, star ratings).
    • Improve AI Recommendations:
      • Use the feedback to improve the AI's future recommendations.
      • Store feedback data securely and use it to refine the assistant's responses.
  7. Analytics and Monitoring:
    • Usage Analytics:
      • Implement analytics to monitor app usage, popular features, and user engagement.
      • Use tools like Google Analytics or Mixpanel.
    • Error Monitoring:
      • Use monitoring tools like Sentry to track and fix errors in real-time.
  8. Advanced AI Features:
    • Contextual Understanding:
      • Enhance the AI's ability to understand context over longer conversations.
      • Use conversation summarization techniques to manage long-term context without exceeding token limits.
    • Natural Language Understanding:
      • Incorporate Named Entity Recognition (NER) to better understand user inputs (e.g., recognizing place names, dates).
  9. Compliance and Legal Considerations:
    • Terms of Service and Privacy Policy:
      • Clearly state how user data is used, stored, and protected.
      • Ensure compliance with Twitter's Developer Agreement and Policy.
    • Data Protection:
      • Store minimal personal data required for the app's functionality.
      • Implement secure data storage practices (e.g., encryption at rest and in transit).
  10. Testing and Quality Assurance:
    • Automated Testing:
      • Implement unit tests and integration tests to ensure code reliability.
      • Use testing frameworks like Jest for JavaScript/TypeScript.
    • User Testing:
      • Conduct user testing sessions to gather feedback on the app's usability and features.
  11. Scalability Considerations:
    • Database Optimization:
      • If you're using a database for the curated list or user data, ensure it's optimized for scalability.
    • API Rate Limits:
      • Monitor and handle API rate limits for OpenAI, Mem0, and Twitter APIs.
  12. Documentation:
    • Code Documentation:
      • Comment your code and maintain documentation for future developers or contributors.
    • User Guides:
      • Provide a help section or FAQ within the app to assist users.
  13. Accessibility:
    • Accessible Design:
      • Ensure the app meets accessibility standards (e.g., WCAG) so that it's usable by people with disabilities.
    • Keyboard Navigation and Screen Readers:
      • Test the app with screen readers and ensure all functionalities are accessible via keyboard.
  14. Marketing and User Acquisition:
    • Social Media Integration:
      • Encourage users to share their experiences on social media platforms beyond Twitter.
    • SEO Optimization:
      • Optimize the app for search engines if it's web-based.
  15. Future Feature Ideas:
    • Booking Integration:
      • Allow users to book hotels or restaurants directly through the app by integrating with booking platforms.
    • Notifications:
      • Implement email or push notifications for updates or personalized offers.
    • Community Features:
      • Create a community space where users can share their own travel tips or experiences.

Conclusion:

Overall, your app integrates several powerful features that provide personalized travel recommendations to users. By focusing on enhancing user experience, ensuring security and compliance, and adding functionalities that allow users to manage their preferences and interact more deeply with the app, you can significantly improve the app.

If there's anything specific you'd like to focus on or if you have other ideas, I'd be happy to help you explore those further.


Below is the final optimized full code for your Next.js app, integrating all the features you've requested:

  • User Authentication with Twitter (X)
  • Personalized AI Travel Assistant using Mem0
  • Integration of your curated list of 1000 hotels and restaurants
  • Leveraging OpenAI's o1-preview model via OpenRouter
  • Ability to tweet recommendations directly from the app

Prerequisites

  1. Environment Variables: Ensure your .env.local file contains all necessary environment variables.

Install Required Packages:

npm install next-auth mem0ai @ai-sdk/openai ai twitter-api-v2 react-hook-form

Project Structure

/pages
  /api
    /auth
      [...nextauth].ts
    chat.ts
    tweet.ts
  chat.tsx
  dashboard.tsx
  login.tsx
/lib
  personalTravelAssistant.ts
  twitter.ts
/types
  messages
    messages-types.ts
/data
  places.json

1. Environment Setup

.env.local

# OpenAI/OpenRouter
OPENAI_API_KEY=<your-openrouter-api-key>
OPENAI_BASE_URL=https://openrouter.ai/api/v1

# Mem0
MEM0_API_KEY=<your-mem0-api-key>

# Twitter (X) API Keys
TWITTER_CLIENT_ID=<your-twitter-client-id>
TWITTER_CLIENT_SECRET=<your-twitter-client-secret>
TWITTER_BEARER_TOKEN=<your-twitter-bearer-token>

2. Types

/types/messages/messages-types.ts

export type Message = {
  role: 'user' | 'assistant';
  content: string;
};

3. Curated Places Data

/data/places.json

Place your curated list of 1000 hotels and restaurants in this JSON file. Here's a sample structure:

[
  {
    "name": "Le Potager du Marais",
    "location": "Paris, France",
    "type": "restaurant",
    "features": ["vegetarian", "organic", "gluten-free"]
  },
  {
    "name": "The Peninsula",
    "location": "Hong Kong",
    "type": "hotel",
    "features": ["luxury", "spa", "sea view"]
  },
  {
    "name": "Gentle Gourmet",
    "location": "Paris, France",
    "type": "restaurant",
    "features": ["vegan", "gluten-free"]
  }
  // ... add the rest of your curated places
]

4. Authentication Setup

/pages/api/auth/[...nextauth].ts

import NextAuth from 'next-auth';
import TwitterProvider from 'next-auth/providers/twitter';

export default NextAuth({
  providers: [
    TwitterProvider({
      clientId: process.env.TWITTER_CLIENT_ID!,
      clientSecret: process.env.TWITTER_CLIENT_SECRET!,
      version: '2.0',
    }),
  ],
  callbacks: {
    async jwt({ token, account }) {
      if (account) {
        token.accessToken = account.access_token;
      }
      return token;
    },
    async session({ session, token }) {
      session.accessToken = token.accessToken;
      return session;
    },
  },
});

5. OpenAI and Mem0 Integration

/lib/personalTravelAssistant.ts

"use server";

import { Message } from "@/types/messages/messages-types";
import { createOpenAI } from "@ai-sdk/openai";
import { generateText } from "ai";
import MemoryClient from "mem0ai";
import places from "@/data/places.json"; // Import curated places

// Initialize OpenAI via OpenRouter
const baseURL = process.env.OPENAI_BASE_URL || "https://api.openai.com/v1";
const apiKey = process.env.OPENAI_API_KEY;

if (!apiKey) {
  throw new Error("Missing OpenRouter API key in environment variables.");
}

const openai = createOpenAI({
  baseURL,
  apiKey,
});

// Initialize Mem0 client
const mem0Client = new MemoryClient(process.env.MEM0_API_KEY);

export class PersonalTravelAssistant {
  private messages: Message[] = [
    {
      role: "system",
      content: `
      You are **TravelerBot**, a helpful, friendly, and well-traveled AI assistant.
      - You provide insightful and detailed travel advice, tailored to the user's preferences.
      - You are enthusiastic about helping users explore new destinations, offering personalized tips, and suggesting hidden gems.
      - You remember past conversations and always build upon them to offer even better suggestions.
      `,
    },
  ];

  // Generate a message using the o1-preview model via OpenRouter
  async askQuestion(question: string, userId: string) {
    // Retrieve previous memories using Mem0
    const previousMemories = await this.getMemories(userId);

    let prompt = question;
    if (previousMemories.length > 0) {
      prompt = `User input: ${question}\nPrevious preferences: ${previousMemories.join(
        ", "
      )}`;
    }

    this.messages.push({ role: "user", content: prompt });

    // Search curated places
    const recommendations = this.searchCuratedPlaces(question, previousMemories);

    if (recommendations.length > 0) {
      const recommendationsText = recommendations
        .map(
          (place) =>
            `${place.name} in ${place.location} (${place.type}) - Features: ${place.features.join(
              ", "
            )}`
        )
        .join("\n");
      prompt += `\nBased on your preferences, here are some recommendations:\n${recommendationsText}`;
    }

    // Generate response from OpenAI's o1-preview model
    const response = await generateText({
      model: openai("o1-preview"),
      messages: this.messages,
    });

    const answer = response.text;
    this.messages.push({ role: "assistant", content: answer });

    // Store the conversation in Mem0
    await this.storeMemory(
      [
        { role: "user", content: question },
        { role: "assistant", content: answer },
      ],
      userId
    );

    return answer;
  }

  // Search the curated places based on user input and memories
  searchCuratedPlaces(query: string, memories: string[]) {
    const queryLower = query.toLowerCase();
    const matchingPlaces = places.filter((place) => {
      const matchesQuery =
        place.name.toLowerCase().includes(queryLower) ||
        place.location.toLowerCase().includes(queryLower) ||
        place.features.some((feature) => feature.toLowerCase().includes(queryLower));

      const matchesPreferences = memories.some((memory) =>
        place.features.some((feature) =>
          feature.toLowerCase().includes(memory.toLowerCase())
        )
      );

      return matchesQuery || matchesPreferences;
    });

    return matchingPlaces;
  }

  // Store user preferences and conversation in Mem0
  async storeMemory(messages: Message[], userId: string) {
    try {
      await mem0Client.add(messages, { user_id: userId });
    } catch (error) {
      console.error("Error storing memory:", error);
    }
  }

  // Retrieve memories (previous user preferences) from Mem0
  async getMemories(userId: string) {
    try {
      const response = await mem0Client.get({ user_id: userId });
      return response.memories.map((memory: any) => memory.content);
    } catch (error) {
      console.error("Error retrieving memories:", error);
      return [];
    }
  }
}

6. Twitter API Integration

/lib/twitter.ts

import { TwitterApi } from 'twitter-api-v2';

export async function tweetRecommendation(accessToken: string, text: string) {
  try {
    const client = new TwitterApi(accessToken);
    const tweet = await client.v2.tweet(text);
    return tweet;
  } catch (error) {
    console.error('Error tweeting:', error);
    throw new Error('Failed to post tweet');
  }
}

7. API Routes

/pages/api/chat.ts

import { NextApiRequest, NextApiResponse } from 'next';
import { PersonalTravelAssistant } from '@/lib/personalTravelAssistant';

const aiAssistant = new PersonalTravelAssistant();

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  const { message, userId } = req.body;

  if (!message || !userId) {
    return res.status(400).json({ error: 'Message and userId are required' });
  }

  try {
    // Generate response while utilizing stored user preferences and curated list
    const answer = await aiAssistant.askQuestion(message, userId);
    return res.status(200).json({ answer });
  } catch (error) {
    console.error("Error in chat:", error);
    return res.status(500).json({ error: 'Error generating answer' });
  }
}

/pages/api/tweet.ts

import { NextApiRequest, NextApiResponse } from 'next';
import { tweetRecommendation } from '@/lib/twitter';
import { getToken } from 'next-auth/jwt';

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  const token = await getToken({ req });

  if (!token || !token.accessToken) {
    return res.status(401).json({ error: 'Not authenticated' });
  }

  const { text } = req.body;

  if (!text) {
    return res.status(400).json({ error: 'Text is required' });
  }

  try {
    const tweet = await tweetRecommendation(token.accessToken as string, text);
    return res.status(200).json({ success: true, tweet });
  } catch (error) {
    console.error('Error tweeting:', error);
    return res.status(500).json({ error: 'Failed to post tweet' });
  }
}

8. Frontend Pages

/pages/login.tsx

import { signIn, useSession } from 'next-auth/react';
import { useEffect } from 'react';

export default function LoginPage() {
  const { data: session } = useSession();

  useEffect(() => {
    if (session) {
      window.location.href = '/dashboard';
    }
  }, [session]);

  return (
    <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', marginTop: '100px' }}>
      <h1>Login with Twitter</h1>
      <button
        onClick={() => signIn('twitter')}
        style={{
          padding: '10px 20px',
          borderRadius: '5px',
          backgroundColor: '#1DA1F2',
          color: '#fff',
          border: 'none',
          cursor: 'pointer',
        }}>
        Sign in with Twitter
      </button>
    </div>
  );
}

/pages/dashboard.tsx

import { useSession, signOut } from 'next-auth/react';
import Link from 'next/link';

export default function DashboardPage() {
  const { data: session } = useSession();

  if (!session) {
    return <p>Loading...</p>;
  }

  return (
    <div>
      <h1>Welcome, {session.user?.name}!</h1>
      <p>Your Twitter handle: {session.user?.username}</p>
      <Link href="/chat">
        <a>Chat with the AI Travel Assistant</a>
      </Link>
      <button onClick={() => signOut()}>Logout</button>
    </div>
  );
}

/pages/chat.tsx

import { useSession } from 'next-auth/react';
import { useState } from 'react';
import { useForm } from 'react-hook-form';

type Message = {
  sender: 'user' | 'agent';
  text: string;
};

export default function Chat() {
  const { data: session } = useSession();
  const { register, handleSubmit, reset } = useForm<{ message: string }>();
  const [messages, setMessages] = useState<Message[]>([]);
  
  const onSubmit = async (data: { message: string }) => {
    const userId = session?.user?.email || 'anonymous';

    // Add user message to the chat
    setMessages((prev) => [...prev, { sender: 'user', text: data.message }]);

    // Send message to backend API
    const response = await fetch('/api/chat', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ message: data.message, userId }),
    });

    const result = await response.json();

    // Add agent's reply to the chat
    setMessages((prev) => [...prev, { sender: 'agent', text: result.answer }]);

    reset();
  };

  const tweetRecommendation = async (text: string) => {
    try {
      const response = await fetch('/api/tweet', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ text }),
      });
      const result = await response.json();
      if (result.success) {
        alert('Successfully tweeted!');
      } else {
        alert('Failed to tweet.');
      }
    } catch (error) {
      console.error('Error posting tweet:', error);
      alert('An error occurred while tweeting.');
    }
  };

  if (!session) {
    return <p>Please log in to interact with the travel agent.</p>;
  }

  return (
    <div className="chat-container">
      <h1>Chat with your Personal Travel Assistant</h1>
      <div className="chat-box">
        {messages.map((message, index) => (
          <div key={index} className={`message ${message.sender}`}>
            <span>{message.sender === 'user' ? 'You' : 'Agent'}:</span> {message.text}
            {message.sender === 'agent' && (
              <button onClick={() => tweetRecommendation(message.text)}>
                Tweet this recommendation
              </button>
            )}
          </div>
        ))}
      </div>

      <form onSubmit={handleSubmit(onSubmit)} className="chat-input">
        <input
          type="text"
          placeholder="Ask your assistant..."
          {...register('message', { required: true })}
        />
        <button type="submit">Send</button>
      </form>

      <style jsx>{`
        .chat-container {
          max-width: 600px;
          margin: 0 auto;
          padding: 20px;
        }
        .chat-box {
          border: 1px solid #ccc;
          padding: 20px;
          height: 400px;
          overflow-y: scroll;
          margin-bottom: 10px;
        }
        .message {
          margin-bottom: 10px;
          display: flex;
          justify-content: space-between;
        }
        .message.user {
          text-align: right;
        }
        .message.agent {
          text-align: left;
        }
        .chat-input {
          display: flex;
        }
        .chat-input input {
          flex: 1;
          padding: 10px;
          border: 1px solid #ccc;
        }
        .chat-input button {
          padding: 10px 20px;
          background-color: #1da1f2;
          color: white;
          border: none;
          cursor: pointer;
        }
      `}</style>
    </div>
  );
}

9. Running the App

Start your development server:

npm run dev

10. Additional Notes

  • Error Handling: Ensure that all API interactions handle errors gracefully, and provide user-friendly error messages.
  • Security: Protect user data by implementing secure data storage practices and adhering to privacy regulations.
  • Performance Optimization: Consider using caching mechanisms if necessary and ensure that API calls are handled asynchronously to prevent UI blocking.
  • User Preferences Management: You may extend the app to include a profile page where users can manage their preferences.

Conclusion

This finalized code provides a fully functional Next.js application that:

  • Allows users to log in with their Twitter (X) account.
  • Provides a chat interface with a Personalized AI Travel Assistant that:
    • Remembers user preferences using Mem0.
    • Leverages your curated list of top 1000 hotels and restaurants.
    • Generates responses using OpenAI's o1-preview model via OpenRouter.
  • Enables users to tweet recommendations directly from the chat interface.

By integrating all these features, the app offers personalized and context-aware travel recommendations, enhancing user experience and engagement.


Absolutely, incorporating images into your recommendations will enhance user engagement and make the tweets more appealing. Below are the necessary code modifications to achieve this:

  1. Update your curated places data to include image URLs.
  2. Modify the PersonalTravelAssistant class to handle images.
  3. Update the chat UI to display images with the recommendations.
  4. Modify the tweet functionality to include images when tweeting recommendations.

1. Update Curated Places Data with Images

/data/places.json

Add an images field to each place in your curated list. Ensure that each place has at least four image URLs.

[
  {
    "name": "Le Potager du Marais",
    "location": "Paris, France",
    "type": "restaurant",
    "features": ["vegetarian", "organic", "gluten-free"],
    "images": [
      "https://example.com/images/le-potager-1.jpg",
      "https://example.com/images/le-potager-2.jpg",
      "https://example.com/images/le-potager-3.jpg",
      "https://example.com/images/le-potager-4.jpg"
    ]
  },
  {
    "name": "The Peninsula",
    "location": "Hong Kong",
    "type": "hotel",
    "features": ["luxury", "spa", "sea view"],
    "images": [
      "https://example.com/images/peninsula-1.jpg",
      "https://example.com/images/peninsula-2.jpg",
      "https://example.com/images/peninsula-3.jpg",
      "https://example.com/images/peninsula-4.jpg"
    ]
  }
  // ... add images for the rest of your curated places
]

2. Modify the PersonalTravelAssistant Class

Update the searchCuratedPlaces method to include images in the recommendations, and adjust the assistant's response to include image URLs.

/lib/personalTravelAssistant.ts

// ... (other imports)
import { Message } from "@/types/messages/messages-types";
import places from "@/data/places.json"; // Ensure this now includes images

export class PersonalTravelAssistant {
  // ... (previous code)

  async askQuestion(question: string, userId: string) {
    // Retrieve previous memories using Mem0
    const previousMemories = await this.getMemories(userId);

    let prompt = question;
    if (previousMemories.length > 0) {
      prompt = `User input: ${question}\nPrevious preferences: ${previousMemories.join(", ")}`;
    }

    this.messages.push({ role: "user", content: prompt });

    // Search curated places
    const recommendations = this.searchCuratedPlaces(question, previousMemories);

    if (recommendations.length > 0) {
      // Prepare the assistant's response with image URLs
      const recommendationsText = recommendations.map((place) => {
        const imagesText = place.images.map((url) => `![${place.name}](${url})`).join("\n");
        return `**${place.name}** in ${place.location} (${place.type})\nFeatures: ${place.features.join(", ")}\n${imagesText}`;
      }).join("\n\n");

      prompt += `\nBased on your preferences, here are some recommendations:\n${recommendationsText}`;
    }

    // Generate response from OpenAI's o1-preview model
    const response = await generateText({
      model: openai("o1-preview"),
      messages: this.messages,
    });

    const answer = response.text;
    this.messages.push({ role: "assistant", content: answer });

    // Store the conversation in Mem0
    await this.storeMemory(
      [
        { role: "user", content: question },
        { role: "assistant", content: answer },
      ],
      userId
    );

    return answer;
  }

  // ... (rest of the class)
}

Note: In the assistant's response, we include image URLs in a markdown-like format. We'll parse and display these images in the chat UI.


3. Update the Chat UI to Display Images

Modify the chat component to parse the assistant's response and display images accordingly.

/pages/chat.tsx

import { useSession } from 'next-auth/react';
import { useState } from 'react';
import { useForm } from 'react-hook-form';
import ReactMarkdown from 'react-markdown';

type Message = {
  sender: 'user' | 'agent';
  text: string;
};

export default function Chat() {
  const { data: session } = useSession();
  const { register, handleSubmit, reset } = useForm<{ message: string }>();
  const [messages, setMessages] = useState<Message[]>([]);

  const onSubmit = async (data: { message: string }) => {
    const userId = session?.user?.email || 'anonymous';

    // Add user message to the chat
    setMessages((prev) => [...prev, { sender: 'user', text: data.message }]);

    // Send message to backend API
    const response = await fetch('/api/chat', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ message: data.message, userId }),
    });

    const result = await response.json();

    // Add agent's reply to the chat
    setMessages((prev) => [...prev, { sender: 'agent', text: result.answer }]);

    reset();
  };

  const tweetRecommendation = async (text: string) => {
    try {
      const response = await fetch('/api/tweet', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ text }),
      });
      const result = await response.json();
      if (result.success) {
        alert('Successfully tweeted!');
      } else {
        alert('Failed to tweet.');
      }
    } catch (error) {
      console.error('Error posting tweet:', error);
      alert('An error occurred while tweeting.');
    }
  };

  if (!session) {
    return <p>Please log in to interact with the travel agent.</p>;
  }

  return (
    <div className="chat-container">
      <h1>Chat with your Personal Travel Assistant</h1>
      <div className="chat-box">
        {messages.map((message, index) => (
          <div key={index} className={`message ${message.sender}`}>
            <span>{message.sender === 'user' ? 'You' : 'Agent'}:</span>
            <div>
              <ReactMarkdown>{message.text}</ReactMarkdown>
            </div>
            {message.sender === 'agent' && (
              <button onClick={() => tweetRecommendation(message.text)}>
                Tweet this recommendation
              </button>
            )}
          </div>
        ))}
      </div>

      <form onSubmit={handleSubmit(onSubmit)} className="chat-input">
        <input
          type="text"
          placeholder="Ask your assistant..."
          {...register('message', { required: true })}
        />
        <button type="submit">Send</button>
      </form>

      <style jsx>{`
        .chat-container { /* ... */ }
        .chat-box { /* ... */ }
        .message { /* ... */ }
        .chat-input { /* ... */ }
      `}</style>
    </div>
  );
}

Notes:

  • We use ReactMarkdown to render markdown content, which includes images.

Install react-markdown if you haven't already:

npm install react-markdown

4. Modify the Tweet Functionality to Include Images

Update the tweetRecommendation function to upload images to Twitter and include them in the tweet.

/lib/twitter.ts

import { TwitterApi } from 'twitter-api-v2';
import axios from 'axios';

export async function tweetRecommendation(accessToken: string, text: string) {
  try {
    const client = new TwitterApi(accessToken);
    const textWithoutImages = text.replace(/!\[.*?\]\(.*?\)/g, ''); // Remove markdown images from text

    // Extract image URLs from the text
    const imageUrls = Array.from(text.matchAll(/!\[.*?\]\((.*?)\)/g)).map(match => match[1]);

    // Upload images to Twitter
    const mediaIds = [];
    for (const imageUrl of imageUrls.slice(0, 4)) { // Limit to 4 images
      const response = await axios.get(imageUrl, { responseType: 'arraybuffer' });
      const mediaId = await client.v1.uploadMedia(Buffer.from(response.data), { mimeType: 'image/jpeg' });
      mediaIds.push(mediaId);
    }

    // Post the tweet with media
    const tweet = await client.v2.tweet(textWithoutImages, { media: { media_ids: mediaIds } });
    return tweet;
  } catch (error) {
    console.error('Error tweeting:', error);
    throw new Error('Failed to post tweet');
  }
}

Notes:

  • Media Upload: Twitter API v2 allows you to upload media and attach it to tweets.
  • Image Download: We use axios to download the images from the URLs.
  • Limit to 4 Images: Twitter allows a maximum of 4 images per tweet.
  • Text Processing: We remove image markdown syntax from the tweet text to avoid clutter.

Install Axios:

npm install axios

5. Update the Tweet API Route

Ensure that the tweet API route handles the new functionality.

/pages/api/tweet.ts

import { NextApiRequest, NextApiResponse } from 'next';
import { tweetRecommendation } from '@/lib/twitter';
import { getToken } from 'next-auth/jwt';

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  const token = await getToken({ req });

  if (!token || !token.accessToken) {
    return res.status(401).json({ error: 'Not authenticated' });
  }

  const { text } = req.body;

  if (!text) {
    return res.status(400).json({ error: 'Text is required' });
  }

  try {
    const tweet = await tweetRecommendation(token.accessToken as string, text);
    return res.status(200).json({ success: true, tweet });
  } catch (error) {
    console.error('Error tweeting:', error);
    return res.status(500).json({ error: 'Failed to post tweet' });
  }
}

6. Testing and Final Touches

Testing:

  • Chat Interface:
    • Interact with the AI assistant and confirm that images are displayed alongside recommendations.
  • Tweet Functionality:
    • Click on "Tweet this recommendation" and ensure that the tweet is posted with images.

Notes:

  • Twitter API Rate Limits: Be aware of Twitter API rate limits and policies regarding media uploads.
  • Image Rights and Licensing:
    • Ensure you have the rights to use and distribute the images in your app and tweets.
    • Avoid using copyrighted images without permission.

Error Handling:

  • Image Download Failures:
    • Implement try-catch blocks around image downloads to handle failures gracefully.
  • User Feedback:
    • Provide clear feedback to the user if the tweet fails to post.

Conclusion

By making these modifications, your app will now:

  • Display images with recommendations in the chat interface, enhancing the user experience.
  • Include images when tweeting recommendations, making tweets more engaging and likely to go viral.

This visual enhancement leverages your curated data and provides users with a richer interaction with your AI Travel Assistant.


Certainly! Below is the complete, improved, and optimized code for your Next.js app. This version includes:

  • User Authentication with Twitter (X)
  • Personalized AI Travel Assistant using Mem0
  • Integration of your curated list of 1000 hotels and restaurants with images
  • Leveraging OpenAI's o1-preview model via OpenRouter
  • Ability to tweet recommendations with images directly from the app

This code is organized and optimized for performance and scalability. Please replace placeholder values (like API keys) with your actual data.


Prerequisites

1. Environment Variables

Ensure your .env.local file contains all necessary environment variables:

# OpenAI/OpenRouter
OPENAI_API_KEY=your-openrouter-api-key
OPENAI_BASE_URL=https://openrouter.ai/api/v1

# Mem0
MEM0_API_KEY=your-mem0-api-key

# Twitter (X) API Keys
TWITTER_CLIENT_ID=your-twitter-client-id
TWITTER_CLIENT_SECRET=your-twitter-client-secret
TWITTER_BEARER_TOKEN=your-twitter-bearer-token

2. Install Required Packages

In your project directory, install the necessary packages:

npm install next-auth mem0ai @ai-sdk/openai ai twitter-api-v2 react-hook-form react-markdown axios

Project Structure

/pages
  /api
    /auth
      [...nextauth].ts
    chat.ts
    tweet.ts
  chat.tsx
  dashboard.tsx
  login.tsx
/lib
  personalTravelAssistant.ts
  twitter.ts
/types
  messages
    messages-types.ts
/data
  places.json

Code Implementation

1. Types

/types/messages/messages-types.ts

export type Message = {
  role: 'user' | 'assistant';
  content: string;
};

2. Curated Places Data

/data/places.json

Please ensure your places.json file includes an images array for each place, with at least four image URLs.

Sample structure:

[
  {
    "name": "Le Potager du Marais",
    "location": "Paris, France",
    "type": "restaurant",
    "features": ["vegetarian", "organic", "gluten-free"],
    "images": [
      "https://example.com/images/le-potager-1.jpg",
      "https://example.com/images/le-potager-2.jpg",
      "https://example.com/images/le-potager-3.jpg",
      "https://example.com/images/le-potager-4.jpg"
    ]
  },
  {
    "name": "The Peninsula",
    "location": "Hong Kong",
    "type": "hotel",
    "features": ["luxury", "spa", "sea view"],
    "images": [
      "https://example.com/images/peninsula-1.jpg",
      "https://example.com/images/peninsula-2.jpg",
      "https://example.com/images/peninsula-3.jpg",
      "https://example.com/images/peninsula-4.jpg"
    ]
  }
  // ... add the rest of your curated places
]

3. Authentication Setup

/pages/api/auth/[...nextauth].ts

import NextAuth from 'next-auth';
import TwitterProvider from 'next-auth/providers/twitter';

export default NextAuth({
  providers: [
    TwitterProvider({
      clientId: process.env.TWITTER_CLIENT_ID!,
      clientSecret: process.env.TWITTER_CLIENT_SECRET!,
      version: '2.0',
    }),
  ],
  callbacks: {
    async jwt({ token, account }) {
      if (account) {
        token.accessToken = account.access_token;
      }
      return token;
    },
    async session({ session, token }) {
      session.accessToken = token.accessToken;
      return session;
    },
  },
});

4. OpenAI and Mem0 Integration

/lib/personalTravelAssistant.ts

"use server";

import { Message } from "@/types/messages/messages-types";
import { createOpenAI } from "@ai-sdk/openai";
import { generateText } from "ai";
import MemoryClient from "mem0ai";
import places from "@/data/places.json"; // Import curated places with images

// Initialize OpenAI via OpenRouter
const baseURL = process.env.OPENAI_BASE_URL || "https://api.openai.com/v1";
const apiKey = process.env.OPENAI_API_KEY;

if (!apiKey) {
  throw new Error("Missing OpenRouter API key in environment variables.");
}

const openai = createOpenAI({
  baseURL,
  apiKey,
});

// Initialize Mem0 client
const mem0Client = new MemoryClient(process.env.MEM0_API_KEY);

export class PersonalTravelAssistant {
  private messages: Message[] = [
    {
      role: "system",
      content: `
      You are **TravelerBot**, a helpful, friendly, and well-traveled AI assistant.
      - You provide insightful and detailed travel advice, tailored to the user's preferences.
      - You are enthusiastic about helping users explore new destinations, offering personalized tips, and suggesting hidden gems.
      - You remember past conversations and always build upon them to offer even better suggestions.
      `,
    },
  ];

  // Generate a message using the o1-preview model via OpenRouter
  async askQuestion(question: string, userId: string) {
    // Retrieve previous memories using Mem0
    const previousMemories = await this.getMemories(userId);

    let prompt = question;
    if (previousMemories.length > 0) {
      prompt = `User input: ${question}\nPrevious preferences: ${previousMemories.join(", ")}`;
    }

    this.messages.push({ role: "user", content: prompt });

    // Search curated places
    const recommendations = this.searchCuratedPlaces(question, previousMemories);

    if (recommendations.length > 0) {
      // Prepare the assistant's response with image URLs
      const recommendationsText = recommendations.map((place) => {
        const imagesText = place.images
          .slice(0, 4) // Limit to 4 images
          .map((url) => `![${place.name}](${url})`)
          .join("\n");
        return `**${place.name}** in ${place.location} (${place.type})\nFeatures: ${place.features.join(", ")}\n${imagesText}`;
      }).join("\n\n");

      prompt += `\nBased on your preferences, here are some recommendations:\n${recommendationsText}`;
    }

    // Generate response from OpenAI's o1-preview model
    const response = await generateText({
      model: openai("o1-preview"),
      messages: this.messages,
    });

    const answer = response.text;
    this.messages.push({ role: "assistant", content: answer });

    // Store the conversation in Mem0
    await this.storeMemory(
      [
        { role: "user", content: question },
        { role: "assistant", content: answer },
      ],
      userId
    );

    return answer;
  }

  // Search the curated places based on user input and memories
  searchCuratedPlaces(query: string, memories: string[]) {
    const queryLower = query.toLowerCase();
    const matchingPlaces = places.filter((place) => {
      const matchesQuery =
        place.name.toLowerCase().includes(queryLower) ||
        place.location.toLowerCase().includes(queryLower) ||
        place.features.some((feature) => feature.toLowerCase().includes(queryLower));

      const matchesPreferences = memories.some((memory) =>
        place.features.some((feature) =>
          feature.toLowerCase().includes(memory.toLowerCase())
        )
      );

      return matchesQuery || matchesPreferences;
    });

    return matchingPlaces;
  }

  // Store user preferences and conversation in Mem0
  async storeMemory(messages: Message[], userId: string) {
    try {
      await mem0Client.add(messages, { user_id: userId });
    } catch (error) {
      console.error("Error storing memory:", error);
    }
  }

  // Retrieve memories (previous user preferences) from Mem0
  async getMemories(userId: string) {
    try {
      const response = await mem0Client.get({ user_id: userId });
      return response.memories.map((memory: any) => memory.content);
    } catch (error) {
      console.error("Error retrieving memories:", error);
      return [];
    }
  }
}

5. Twitter API Integration

/lib/twitter.ts

import { TwitterApi } from 'twitter-api-v2';
import axios from 'axios';

export async function tweetRecommendation(accessToken: string, text: string) {
  try {
    const client = new TwitterApi(accessToken);
    const textWithoutImages = text.replace(/!\[.*?\]\(.*?\)/g, ''); // Remove markdown images from text

    // Extract image URLs from the text
    const imageUrls = Array.from(text.matchAll(/!\[.*?\]\((.*?)\)/g)).map(match => match[1]);

    // Upload images to Twitter
    const mediaIds = [];
    for (const imageUrl of imageUrls.slice(0, 4)) { // Limit to 4 images
      try {
        const response = await axios.get(imageUrl, { responseType: 'arraybuffer' });
        const mediaId = await client.v1.uploadMedia(Buffer.from(response.data), { mimeType: 'image/jpeg' });
        mediaIds.push(mediaId);
      } catch (error) {
        console.error(`Failed to upload image: ${imageUrl}`, error);
        // Continue uploading other images
      }
    }

    // Post the tweet with media
    const tweet = await client.v2.tweet(textWithoutImages, { media: { media_ids: mediaIds } });
    return tweet;
  } catch (error) {
    console.error('Error tweeting:', error);
    throw new Error('Failed to post tweet');
  }
}

Note:

  • Image Uploads: The code uploads images to Twitter and associates them with the tweet.
  • Error Handling: The inner try-catch block within the image upload loop handles any failures in uploading individual images without stopping the entire process.

6. API Routes

/pages/api/chat.ts

import { NextApiRequest, NextApiResponse } from 'next';
import { PersonalTravelAssistant } from '@/lib/personalTravelAssistant';

const aiAssistant = new PersonalTravelAssistant();

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  const { message, userId } = req.body;

  if (!message || !userId) {
    return res.status(400).json({ error: 'Message and userId are required' });
  }

  try {
    // Generate response while utilizing stored user preferences and curated list
    const answer = await aiAssistant.askQuestion(message, userId);
    return res.status(200).json({ answer });
  } catch (error) {
    console.error("Error in chat:", error);
    return res.status(500).json({ error: 'Error generating answer' });
  }
}

/pages/api/tweet.ts

import { NextApiRequest, NextApiResponse } from 'next';
import { tweetRecommendation } from '@/lib/twitter';
import { getToken } from 'next-auth/jwt';

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  const token = await getToken({ req });

  if (!token || !token.accessToken) {
    return res.status(401).json({ error: 'Not authenticated' });
  }

  const { text } = req.body;

  if (!text) {
    return res.status(400).json({ error: 'Text is required' });
  }

  try {
    const tweet = await tweetRecommendation(token.accessToken as string, text);
    return res.status(200).json({ success: true, tweet });
  } catch (error) {
    console.error('Error tweeting:', error);
    return res.status(500).json({ error: 'Failed to post tweet' });
  }
}

7. Frontend Pages

/pages/login.tsx

import { signIn, useSession } from 'next-auth/react';
import { useEffect } from 'react';

export default function LoginPage() {
  const { data: session } = useSession();

  useEffect(() => {
    if (session) {
      window.location.href = '/dashboard';
    }
  }, [session]);

  return (
    <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', marginTop: '100px' }}>
      <h1>Login with Twitter</h1>
      <button
        onClick={() => signIn('twitter')}
        style={{
          padding: '10px 20px',
          borderRadius: '5px',
          backgroundColor: '#1DA1F2',
          color: '#fff',
          border: 'none',
          cursor: 'pointer',
        }}>
        Sign in with Twitter
      </button>
    </div>
  );
}

/pages/dashboard.tsx

import { useSession, signOut } from 'next-auth/react';
import Link from 'next/link';

export default function DashboardPage() {
  const { data: session } = useSession();

  if (!session) {
    return <p>Loading...</p>;
  }

  return (
    <div>
      <h1>Welcome, {session.user?.name}!</h1>
      <p>Your Twitter handle: {session.user?.username}</p>
      <Link href="/chat">
        <a>Chat with the AI Travel Assistant</a>
      </Link>
      <button onClick={() => signOut()}>Logout</button>
    </div>
  );
}

/pages/chat.tsx

import { useSession } from 'next-auth/react';
import { useState } from 'react';
import { useForm } from 'react-hook-form';
import ReactMarkdown from 'react-markdown';

type Message = {
  sender: 'user' | 'agent';
  text: string;
};

export default function Chat() {
  const { data: session } = useSession();
  const { register, handleSubmit, reset } = useForm<{ message: string }>();
  const [messages, setMessages] = useState<Message[]>([]);

  const onSubmit = async (data: { message: string }) => {
    const userId = session?.user?.email || 'anonymous';

    // Add user message to the chat
    setMessages((prev) => [...prev, { sender: 'user', text: data.message }]);

    // Send message to backend API
    const response = await fetch('/api/chat', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ message: data.message, userId }),
    });

    const result = await response.json();

    // Add agent's reply to the chat
    setMessages((prev) => [...prev, { sender: 'agent', text: result.answer }]);

    reset();
  };

  const tweetRecommendation = async (text: string) => {
    try {
      const response = await fetch('/api/tweet', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ text }),
      });
      const result = await response.json();
      if (result.success) {
        alert('Successfully tweeted!');
      } else {
        alert('Failed to tweet.');
      }
    } catch (error) {
      console.error('Error posting tweet:', error);
      alert('An error occurred while tweeting.');
    }
  };

  if (!session) {
    return <p>Please log in to interact with the travel agent.</p>;
  }

  return (
    <div className="chat-container">
      <h1>Chat with your Personal Travel Assistant</h1>
      <div className="chat-box">
        {messages.map((message, index) => (
          <div key={index} className={`message ${message.sender}`}>
            <span>{message.sender === 'user' ? 'You' : 'Agent'}:</span>
            <div>
              <ReactMarkdown>{message.text}</ReactMarkdown>
            </div>
            {message.sender === 'agent' && (
              <button onClick={() => tweetRecommendation(message.text)}>
                Tweet this recommendation
              </button>
            )}
          </div>
        ))}
      </div>

      <form onSubmit={handleSubmit(onSubmit)} className="chat-input">
        <input
          type="text"
          placeholder="Ask your assistant..."
          {...register('message', { required: true })}
        />
        <button type="submit">Send</button>
      </form>

      <style jsx>{`
        .chat-container {
          max-width: 600px;
          margin: 0 auto;
          padding: 20px;
        }
        .chat-box {
          border: 1px solid #ccc;
          padding: 20px;
          height: 400px;
          overflow-y: scroll;
          margin-bottom: 10px;
        }
        .message {
          margin-bottom: 10px;
        }
        .message.user {
          text-align: right;
        }
        .message.agent {
          text-align: left;
        }
        .chat-input {
          display: flex;
        }
        .chat-input input {
          flex: 1;
          padding: 10px;
          border: 1px solid #ccc;
        }
        .chat-input button {
          padding: 10px 20px;
          background-color: #1da1f2;
          color: white;
          border: none;
          cursor: pointer;
        }
        button {
          margin-top: 5px;
        }
        img {
          max-width: 100%;
          height: auto;
          margin-top: 5px;
        }
      `}</style>
    </div>
  );
}

Notes:

  • Image Rendering: We use ReactMarkdown to parse and render markdown content, including images.
  • Styling: Adjusted the styles to ensure images are displayed properly.

8. Running the App

To start your development server, run:

npm run dev

Additional Notes and Optimizations

  • Error Handling:
    • Implement comprehensive error handling in API routes and client-side code to provide a better user experience.
    • Use try-catch blocks where appropriate and inform the user of any issues.
  • Security:
    • Ensure that sensitive data like API keys are not exposed in the client-side code.
    • Use HTTPS in production to encrypt data in transit.
  • Performance Optimization:
    • Cache frequently accessed data, like the curated list, if necessary.
    • Optimize image loading by using smaller image sizes or lazy loading.
  • Data Validation:
    • Validate user inputs on both the client and server sides to prevent injection attacks or invalid data processing.
  • Accessibility:
    • Add ARIA labels and ensure the app is navigable via keyboard for accessibility compliance.
  • Scalability:
    • Consider using a database for the curated list if it's large and may require dynamic updates.
  • Legal Considerations:
    • Ensure you have the rights to use and distribute images included in your curated list.
    • Comply with Twitter's Developer Agreement and policies when using their APIs.

Conclusion

With these modifications, your app now provides a richer user experience by:

  • Displaying images with recommendations in the chat interface.
  • Including images when tweeting recommendations, making the tweets more engaging.
  • Enhancing personalization through Mem0 and your curated list.
  • Leveraging OpenAI's o1-preview model for advanced AI responses.

This complete, optimized codebase is ready for you to implement and test. Remember to replace placeholder values with your actual API keys and data.


TravelerBot - Personalized AI Travel Assistant

Introduction

Elevator Pitch

Welcome to TravelerBot, your personalized AI Travel Assistant! This innovative app combines the power of AI, memory retention, and social sharing to deliver tailored travel recommendations just for you. By logging in with your Twitter (X) account, you can interact with an AI that remembers your preferences, suggests curated hotels and restaurants from our exclusive list of top 1000 places around the world, and even lets you share these recommendations with your followers—all enhanced with captivating images to enrich your experience and boost social engagement.

Features

  • Personalized AI Assistant: Interact with an AI that remembers your past conversations and preferences using Mem0, providing you with highly personalized travel recommendations.
  • Curated Recommendations: Access our exclusive list of 1000 hand-picked hotels and restaurants, carefully selected over 20 years to offer you the best experiences worldwide.
  • Twitter (X) Authentication: Log in securely using your Twitter account to get started quickly.
  • Social Sharing: Tweet your favorite recommendations directly from the app, complete with images to make your posts more engaging.
  • Image-Rich Suggestions: Receive travel recommendations accompanied by stunning images to enhance your decision-making and sharing experience.

Getting Started

Prerequisites

  • Node.js and npm installed on your machine.
  • Twitter Developer account with API keys.
  • OpenRouter API key for OpenAI's o1-preview model.
  • Mem0 API key for memory management.
  • A curated list of places stored in places.json with image URLs.

Installation

Prepare the Curated ListEnsure your places.json file is located in the /data directory and includes images:

[
  {
    "name": "Le Potager du Marais",
    "location": "Paris, France",
    "type": "restaurant",
    "features": ["vegetarian", "organic", "gluten-free"],
    "images": [
      "https://example.com/images/le-potager-1.jpg",
      "https://example.com/images/le-potager-2.jpg",
      "https://example.com/images/le-potager-3.jpg",
      "https://example.com/images/le-potager-4.jpg"
    ]
  },
  // ... rest of your curated places
]

Set Up Environment VariablesCreate a .env.local file in the root directory and add your keys:

# OpenAI/OpenRouter
OPENAI_API_KEY=your-openrouter-api-key
OPENAI_BASE_URL=https://openrouter.ai/api/v1

# Mem0
MEM0_API_KEY=your-mem0-api-key

# Twitter (X) API Keys
TWITTER_CLIENT_ID=your-twitter-client-id
TWITTER_CLIENT_SECRET=your-twitter-client-secret
TWITTER_BEARER_TOKEN=your-twitter-bearer-token

Install Dependencies

npm install

Clone the Repository

git clone https://github.com/yourusername/travelerbot.git
cd travelerbot

Running the App

Start the development server:

npm run dev

Open your browser and navigate to http://localhost:3000 to access the app.

Usage

  1. Log In
    • Navigate to the login page and sign in with your Twitter (X) account.
  2. Interact with TravelerBot
    • Access the chat interface from the dashboard.
    • Start chatting with TravelerBot to receive personalized travel recommendations.
  3. Receive Recommendations
    • TravelerBot will provide suggestions based on your preferences and our curated list.
    • Recommendations include images to enhance your experience.
  4. Tweet Recommendations
    • Click the "Tweet this recommendation" button to share suggestions with your followers.
    • The tweet will include images to make it more engaging.

Project Structure

/pages
  /api
    /auth
      [...nextauth].ts       # Authentication setup
    chat.ts                  # Chat API route
    tweet.ts                 # Tweet API route
  chat.tsx                   # Chat interface
  dashboard.tsx              # User dashboard
  login.tsx                  # Login page
/lib
  personalTravelAssistant.ts # AI assistant logic
  twitter.ts                 # Twitter API integration
/types
  messages
    messages-types.ts        # Type definitions for messages
/data
  places.json                # Curated list of places

Technologies Used

  • Next.js: React framework for server-side rendering and building web applications.
  • NextAuth.js: Authentication for Next.js applications.
  • OpenAI's o1-preview Model via OpenRouter: AI model for generating responses.
  • Mem0: Memory management to retain user preferences and past interactions.
  • Twitter API v2: For authentication and tweeting recommendations.
  • React Hook Form: Form handling in React.
  • React Markdown: Rendering markdown content in React.
  • Axios: Promise-based HTTP client for the browser and Node.js.

Contributing

We welcome contributions! Please follow these steps:

  1. Fork the repository.
  2. Submit a pull request.

Push to the branch:

git push origin feature-name

Make your changes and commit them:

git commit -m 'Add feature'

Create a new branch:

git checkout -b feature-name

License

This project is licensed under the MIT License.

Acknowledgments

  • OpenAI for providing the powerful AI models.
  • Mem0 for the memory management system.
  • Twitter for the API access.
  • All contributors and users who have provided feedback.

Contact

For any questions or suggestions, please contact:


Given your project's structure and the technologies used (Next.js, API routes, environment variables, etc.), the best CLI to deploy this app is the Vercel CLI. Vercel is the company behind Next.js and provides seamless deployment for Next.js applications with support for serverless functions (which you are using for your API routes), environment variables, and other features necessary for your app.

Why Vercel CLI?

  • Optimized for Next.js: Vercel provides first-class support for Next.js applications, ensuring optimal performance and ease of deployment.
  • Serverless Functions: Your API routes (/pages/api/*) are deployed as serverless functions, which Vercel handles efficiently.
  • Environment Variables: Vercel allows you to securely manage environment variables required for your APIs (OpenAI, Mem0, Twitter).
  • Edge Network: Vercel's global edge network ensures fast response times for users worldwide.
  • CLI Convenience: The Vercel CLI allows you to deploy your application directly from the command line, making the deployment process straightforward.

Steps to Deploy Your App Using Vercel CLI

1. Install Vercel CLI

First, install the Vercel CLI globally on your machine:

npm install -g vercel

2. Login to Vercel

If you haven't already, log in to your Vercel account. If you don't have an account, you can sign up for one.

vercel login

This will prompt you to authenticate via the browser.

3. Initialize Your Project with Vercel

From the root directory of your project, initialize your project with Vercel:

vercel

This command will prompt you with some questions:

  • Set up and deploy “~/path-to-your-project”? (Y/n): Press Y to proceed.
  • Which scope do you want to deploy to?: Select your Vercel account or team.
  • Link to existing project?: Since this is a new project, select No, create a new project.
  • What’s your project’s name?: You can accept the default or provide a custom name.
  • In which directory is your code located?: Usually, this is the root directory, so you can press Enter.
  • Want to modify these settings?: Select No.

4. Set Environment Variables

Your application relies on several environment variables. You need to set these in your Vercel project settings.

You can set environment variables via the Vercel CLI or through the Vercel Dashboard.

Option A: Using Vercel CLI
vercel env add [variable name] [value]

Example:

vercel env add OPENAI_API_KEY your-openai-api-key
vercel env add OPENAI_BASE_URL https://openrouter.ai/api/v1
vercel env add MEM0_API_KEY your-mem0-api-key
vercel env add TWITTER_CLIENT_ID your-twitter-client-id
vercel env add TWITTER_CLIENT_SECRET your-twitter-client-secret
vercel env add TWITTER_BEARER_TOKEN your-twitter-bearer-token

Alternatively, you can create a .env file with your environment variables and then use the following command to upload all environment variables at once:

vercel env pull .env
Option B: Using Vercel Dashboard
  1. Go to your Vercel Dashboard at https://vercel.com/dashboard.
  2. Select your project.
  3. Navigate to the Settings tab.
  4. Under Environment Variables, add the variables one by one.

Ensure you add the variables for the Production environment if you intend to deploy to production.

5. Deploy the App

Now that your environment variables are set, you can deploy your app.

vercel --prod

This command will deploy your app to production.

6. Verify the Deployment

After deployment, Vercel will provide you with a URL where your app is hosted. Visit this URL to verify that your app is running correctly.

Additional Considerations

  • API Secrets: Ensure that your API keys and secrets are stored securely and are not committed to your repository.
  • Twitter API Callback URLs: You need to configure your Twitter Developer account to include the Vercel deployment URL in the Callback URLs and Website URL fields. This is necessary for Twitter OAuth to work in a production environment.
    • Go to your Twitter Developer Portal.
    • Navigate to your app's settings.
    • Under User Authentication Settings, add your Vercel app URL (e.g., https://your-app.vercel.app/api/auth/callback/twitter) to the Callback URLs.
  • OpenAI and Mem0 API Usage: Ensure that your API usage complies with the terms of service of OpenAI and Mem0.

Alternative Deployment Options

While Vercel is the recommended platform for deploying Next.js applications due to its seamless integration and optimization, there are other platforms you could consider:

  • Netlify: Supports Next.js with some configuration, but may require additional setup for serverless functions.
  • Heroku: Can run Next.js apps, but may require more configuration, especially for serverless functions.
  • AWS Amplify: Offers hosting for full-stack applications, but setup is more complex.
  • Docker Deployment: You could containerize your application and deploy it on platforms like AWS, GCP, or DigitalOcean.

However, these options might require more configuration and may not offer the same level of integration and ease of use as Vercel for Next.js apps.

Conclusion

Using the Vercel CLI is the most straightforward and efficient way to deploy your Next.js application with the given structure. It provides seamless integration with Next.js features, easy environment variable management, and efficient handling of serverless functions.


If you need further assistance with deployment or have questions about any of the steps, feel free to ask!