Feedback requested: Google Calendar integration (chatgpt-assisted)

Hi all, I made an extension that allows users to create Google Calendar events from text selected by using the OpenAI ChatGPT API to extract event details and generate a calendar URL. I’d love any feedback – it works but I’m sure there are many ways to improve it.

Here’s a snapshot of the sniippet as it exists right now:

// #popclip extension for Google Calendar Event (ChatGPT-assisted)
// name: Google Calendar Event (ChatGPT-assisted)
// icon: symbol:calendar
// language: typescript
// module: true
// popclipVersion: 4226
// entitlements: [network]
// options: [{
//   identifier: apikey, 
//   label: 'API Key', 
//   type: 'string',
//   description: 'Obtain API key from https://platform.openai.com/account/api-keys'
// }]

/**
 * This PopClip extension uses the OpenAI ChatGPT API to extract event details from selected text
 * and create a Google Calendar event URL which can then be opened in the default browser.
 */

// TypeScript type definitions
interface IGenerateCalendarURLInput {
    text: string;
}

interface IOptions {
    apikey: string;
}

interface IEventDetails {
    eventName?: string;
    dates?: {
        start?: string;
        end?: string;
    };
    details?: string;
    location?: string;
}

// Constants
const API_BASE_URL: string = "https://api.openai.com/v1";
const API_MODEL: string = "gpt-3.5-turbo";
const GOOGLE_CALENDAR_BASE_URL: string = "https://calendar.google.com/calendar/render?action=TEMPLATE";
const CHATGPT_INSTRUCTION_TEMPLATE: string = `Analyze the text to extract event details such as the event name, start and end times, details, and location. Format this information in a JSON object with keys: 'eventName', 'dates.start', 'dates.end', 'details', and 'location', using 'YYYY-MM-DDTHH:mm:ss' for date formats. If no specific date is mentioned, use today's date, which is {formattedDate}. Interpret relative dates like 'tomorrow' based on this date. For unspecified durations, default to one hour. If a partial date like 'Tuesday' is mentioned, assume it as the next occurrence of that day from today.`;

const messages: Array<{ role: string; content: string }> = []; // History of previous messages

/**
 * Converts local time to Zulu time (UTC) in the format required for Google Calendar URLs.
 * 
 * @param {string} dateString - The date string to convert to Zulu time.
 * @returns {string} - The date in Zulu time format or an empty string if conversion fails.
 */
function convertToZuluTime(dateString: string): string {
    try {
        const dateObj = new Date(dateString);
        const zuluDate = dateObj.toISOString().replace(/[-:]/g, '').split('.')[0] + 'Z';
        print(`Converting to Zulu time: ${dateString} -> ${zuluDate}`);
        return zuluDate;
    } catch (error) {
        print(`Error converting to Zulu time: ${error.message}`);
        return '';
    }
}

/**
 * Generates a Google Calendar event URL based on the input text using ChatGPT.
 * 
 * @param {IGenerateCalendarURLInput} input - The input text selected by the user.
 * @param {IOptions} options - Options containing the API key.
 * @returns {Promise<string>} - A promise that resolves to the Google Calendar URL.
 */
async function generateCalendarURL(input: IGenerateCalendarURLInput, options: IOptions): Promise<string> {
    const openai = require("axios").create({
        baseURL: API_BASE_URL,
        headers: { Authorization: `Bearer ${options.apikey}` }
    });

    // Construct the message to instruct ChatGPT
    const today = new Date();
    const formattedDate = today.toISOString().split('T')[0];
    const chatGptInstruction = CHATGPT_INSTRUCTION_TEMPLATE.replace("{formattedDate}", formattedDate);
    messages.push({ role: "system", content: chatGptInstruction });
    messages.push({ role: "user", content: input.text });

    try {
        const { data } = await openai.post("/chat/completions", {
            model: API_MODEL,
            messages
        });

        // Handle the API response
        messages.push(data.choices[0].message);
        const parsedResponse: IEventDetails = JSON.parse(data.choices[0].message.content);

        // Construct URL components
        const eventName = parsedResponse.eventName ? `&text=${encodeURIComponent(parsedResponse.eventName)}` : '';
        const startDate = convertToZuluTime(parsedResponse.dates?.start || "");
        const endDate = convertToZuluTime(parsedResponse.dates?.end || "");
        const dateParam = startDate && endDate ? `&dates=${startDate}/${endDate}` : '';
        const details = parsedResponse.details ? `&details=${encodeURIComponent(parsedResponse.details)}` : '';
        const location = parsedResponse.location ? `&location=${encodeURIComponent(parsedResponse.location)}` : '';

        // Build the full URL
        const calendarURL = GOOGLE_CALENDAR_BASE_URL + eventName + dateParam + details + location;
        print(`Generated Calendar URL: ${calendarURL}`);
        popclip.openUrl(calendarURL);
        return calendarURL;

    } catch (error) {
        // Handle specific error for a bad or missing API key
        if (error.response && error.response.status === 401) {
            throw new Error("Settings error: Incorrect or missing API key");
        }
        else {
            print(`Error generating Google Calendar URL: ${error.message}`);
        }
        throw error; // Re-throw the error for any other kind of issue
    }
}

// Exports the PopClip action
exports.actions = [{
    title: "Generate Google Calendar URL",
    code: generateCalendarURL,
}];

1 Like

Wow, great job!

I couldn’t install it at first as a snippet – this is because it is a little over 5000 characters. So I saved it as a text file with .popcliptxt extension and I could install it just fine.

How you have structured the extension’s source code and config header is spot on and I love that you are taking advantage of the new no-build-step TypeScript capability.

I tried it on a document I had lying around, and after thinking, it popped open the Google Calendar webpage with all the fields correctly filled in. I was somewhat startled. It’s really exciting to see this sort of thing.

p.s. on GitHub your readme refers to the .popclipext file, but I think this maybe should be .popcliptxt ?-- but I’m not sure as the file in the repo is (understandably, since its nicer working with files that have their correct language extension) a .js. (I would like to make this all a lot easier for everyone when it comes to sharing creations. I’m working on something that may turn out pretty cool, but putting it together is taking a while.)

Also, since it’s TypeScript (the first TS extension I’ve seen in the wild, btw!), just as a stylistic thing, you can do the export with ES modules syntax if you want:

export const actions = [{
    title: "Generate Google Calendar URL",
    code: generateCalendarURL,
}];

But of course this is purely a style thing and makes no actual difference at all — it transpiles to the same thing under the hood.

1 Like

I’m getting this:

This extension is not compatible with this version of PopClip. Please update to the latest version of PopClip.

Extension: google-calendar-chatgpt-assisted.popcliptxt

What version of PopClip are you using?