Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update ai-text-to-calendar extension #17930

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions extensions/ai-text-to-calendar/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
# AI Text to Calendar Changelog

## [Enhancement] - {PR_MERGE_DATE}

- 🗓️ Outlook Calendar Support - added

## [Bug Fix and Enhancement] - 2025-01-15

- 🐞 Bug fix - preference api call and author id
- 👨‍🍳 Customable service - fill your own LLM service endpoint and model name
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

syntax: 'Customable' is not a word - should be 'Customizable'

Suggested change
- 👨‍🍳 Customable service - fill your own LLM service endpoint and model name
- 👨‍🍳 Customizable service - fill your own LLM service endpoint and model name

- 💬 Multiple Language Support - Set your preferred language for events
Expand Down
6 changes: 4 additions & 2 deletions extensions/ai-text-to-calendar/README.md
Copy link
Contributor

@ViGeng ViGeng Mar 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

now we can check outlook in roadmap 🥳

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done!

Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,14 @@ Configure the extension via `Raycast Settings > Extensions > AI Text to Calendar
| `model` | Model Name | string | false | LLM model name, default is `gpt-4o-mini` |
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

syntax: Model name 'gpt-4o-mini' appears to be a typo (should be 'gpt-4' or similar)

| `language` | Language | string | false | Language of the output text, default is `English` |
| `endpoint` | Endpoint | string | false | LLM service endpoint (e.g., <https://api.deepseek.com/v1>), default is `https://api.openai.com/v1` |
| `calendar` | Calendar | string | false | Select your calendar service, default is `googleCalendar`. |


## TODO

- [ ] User default settings (e.g., default date, time)
- [ ] With supplementary information (e.g., selected text + user input)
- [ ] Support for other calendar services (e.g., use Apple Script or Shortcut for Apple Calendar)
- [x] Support for other calendar services (e.g., use Apple Script or Shortcut for Apple Calendar)

## License

Expand All @@ -37,4 +39,4 @@ This project is licensed under the MIT License.
| `play tennis with Mike tommorrow at 5pm in the park` | ✓ | ✓ |
| `Math class next Monday at 10am in lecture hall` | x | ✓ |

Tips: try to use advanced LLM models for better results, especially for date reasoning.
Tips: try to use advanced LLM models for better results, especially for date reasoning.
85 changes: 72 additions & 13 deletions extensions/ai-text-to-calendar/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
"icon": "extension-icon.png",
"author": "izm51",
"contributors": [
"ViGeng"
"ViGeng",
"matsuyama-k1"
],
"categories": [
"Productivity"
Expand Down Expand Up @@ -49,18 +50,76 @@
"type": "dropdown",
"default": "English",
"data": [
{ "title": "English","value": "English" },
{ "title": "Chinese", "value": "Chinese"},
{ "title": "Japanese", "value": "Japanese"},
{ "title": "Korean", "value": "Korean"},
{ "title": "Spanish", "value": "Spanish"},
{ "title": "French", "value": "French"},
{ "title": "German", "value": "German"},
{ "title": "Italian", "value": "Italian"},
{ "title": "Dutch", "value": "Dutch"},
{ "title": "Portuguese", "value": "Portuguese"},
{ "title": "Russian", "value": "Russian"},
{ "title": "Arabic", "value": "Arabic"}
{
"title": "English",
"value": "English"
},
{
"title": "Chinese",
"value": "Chinese"
},
{
"title": "Japanese",
"value": "Japanese"
},
{
"title": "Korean",
"value": "Korean"
},
{
"title": "Spanish",
"value": "Spanish"
},
{
"title": "French",
"value": "French"
},
{
"title": "German",
"value": "German"
},
{
"title": "Italian",
"value": "Italian"
},
{
"title": "Dutch",
"value": "Dutch"
},
{
"title": "Portuguese",
"value": "Portuguese"
},
{
"title": "Russian",
"value": "Russian"
},
{
"title": "Arabic",
"value": "Arabic"
}
],
"required": false
},
{
"name": "calendar",
"title": "Calendar",
"description": "Calendar to post",
"type": "dropdown",
"default": "googleCalendar",
"data": [
{
"title": "Google calendar",
"value": "googleCalendar"
},
{
"title": "Outlook calendar (Personal)",
"value": "outlookPersonal"
},
{
"title": "Outlook calendar (Office 365)",
"value": "outlookOffice"
}
],
"required": false
}
Expand Down
23 changes: 4 additions & 19 deletions extensions/ai-text-to-calendar/src/ai-text-to-calendar.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { Clipboard, getPreferenceValues, getSelectedText, open, showHUD, showToast, Toast } from "@raycast/api";
import OpenAI from "openai";
import { toURL } from "./calendars";

interface CalendarEvent {
export interface CalendarEvent {
title: string;
start_date: string;
start_time: string;
Expand All @@ -17,6 +18,7 @@ export default async function main() {
const endpoint = getPreferenceValues().endpoint || "https://api.openai.com/v1";
const language = getPreferenceValues().language || "English";
const model = getPreferenceValues().model || "gpt-4o-mini";
const calendar = getPreferenceValues().calendar || "googleCalendar";

showToast({ style: Toast.Style.Animated, title: "Extracting..." });
const selectedText = await getSelectedText();
Comment on lines 23 to 24
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: getSelectedText() should be wrapped in a try-catch block with graceful error handling

Suggested change
showToast({ style: Toast.Style.Animated, title: "Extracting..." });
const selectedText = await getSelectedText();
const selectedText = await getSelectedText().catch(() => {
throw new Error("No text selected. Please select some text and try again.");
});
showToast({ style: Toast.Style.Animated, title: "Extracting..." });

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's better not to throw a new error in the catch statement. We could show a failure toast instead.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This syntax of JS is interesting. How about using try-catch and handling the exception/err?

Expand All @@ -26,7 +28,7 @@ export default async function main() {
}

const calendarEvent = JSON.parse(json) as CalendarEvent;
const url = toURL(calendarEvent);
const url = toURL(calendarEvent, calendar);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] Consider separating calendar-specific URL generation into dedicated methods

Would it be possible to split the toURL function’s switch-case into separate methods? For example:

    let url: string;
    switch (calendar) {
      case "outlookOffice365":
        url = toOutlookOfficeURL(calendarEvent);
        break;
      case "outlookPersonal":
        url = toOutlookPersonalURL(calendarEvent);
        break;
      case "googleCalendar":
      default:
        url = toGoogleCalendarURL(calendarEvent);
    }

This could help improve testability and make the code more maintainable.

Copy link
Author

@matsuyama-k1 matsuyama-k1 Mar 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for your review!
I agree with you.

I tried separating the code that generate url so that compatible calendars can be easily added and implemented.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! 🚀


await showHUD("Extracted! Copied to clipboard and opened in browser.");
await Clipboard.copy(`${url}`);
Expand Down Expand Up @@ -89,20 +91,3 @@ Note:

return response.choices[0].message.content;
}

function toURL(json: CalendarEvent) {
// Clean up and format dates/times - remove any non-numeric characters
const startDateTime = `${json.start_date.replace(/-/g, "")}T${json.start_time.replace(/:/g, "")}00`;
const endDateTime = `${json.end_date.replace(/-/g, "")}T${json.end_time.replace(/:/g, "")}00`;

// Encode parameters for URL safety
const params = {
text: encodeURIComponent(json.title),
dates: `${startDateTime}/${endDateTime}`,
details: encodeURIComponent(json.details),
location: encodeURIComponent(json.location),
};

const url = `https://calendar.google.com/calendar/render?action=TEMPLATE&text=${params.text}&dates=${params.dates}&details=${params.details}&location=${params.location}&trp=false`;
return url;
}
16 changes: 16 additions & 0 deletions extensions/ai-text-to-calendar/src/calendars/googleCalendar.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { CalendarEvent } from "../ai-text-to-calendar";
import { parseDateTimes } from "../utils";

export function toGoolgleCalenderURL(event: CalendarEvent) {
const dateTimes = parseDateTimes(event);

const startDateTime = `${dateTimes.startDate}T${dateTimes.startTime}00`;
const endDateTime = `${dateTimes.endDate}T${dateTimes.endTime}00`;
const params = {
text: encodeURIComponent(event.title),
dates: `${startDateTime}/${endDateTime}`,
details: encodeURIComponent(event.details),
location: encodeURIComponent(event.location),
};
return `https://calendar.google.com/calendar/render?action=TEMPLATE&text=${params.text}&dates=${params.dates}&details=${params.details}&location=${params.location}&trp=false`;
}
19 changes: 19 additions & 0 deletions extensions/ai-text-to-calendar/src/calendars/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { CalendarEvent } from "../ai-text-to-calendar";
import { toGoolgleCalenderURL } from "./googleCalendar";
import { toOutlookOfficeURL, toOutlookPersonalURL } from "./outlookCalendar";

type CalendarType = "googleCalendar" | "outlookPersonal" | "outlookOffice";

export interface CalendarURLGenerator {
(event: CalendarEvent): string;
}
const CALENDAR_URL_GENERATORS: Record<CalendarType, CalendarURLGenerator> = {
googleCalendar: toGoolgleCalenderURL,
outlookPersonal: toOutlookPersonalURL,
outlookOffice: toOutlookOfficeURL,
};

export function toURL(calendarEvent: CalendarEvent, calendarType: CalendarType) {
const generator = CALENDAR_URL_GENERATORS[calendarType];
return generator(calendarEvent);
}
47 changes: 47 additions & 0 deletions extensions/ai-text-to-calendar/src/calendars/outlookCalendar.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { CalendarEvent } from "../ai-text-to-calendar";
import { parseDateTimes } from "../utils";

export function toOutlookOfficeURL(event: CalendarEvent) {
const baseUrl = "https://outlook.office.com/calendar/deeplink/compose";
return baseUrl + createQueryStringOfOutlook(event);
}

export function toOutlookPersonalURL(event: CalendarEvent) {
const baseUrl = "https://outlook.live.com/calendar/deeplink/compose";
return baseUrl + createQueryStringOfOutlook(event);
}

function formatDateTimeForOutlook(dateStr: string, timeStr: string): string {
if (dateStr.length !== 8) {
throw new Error(`Invalid date format: ${dateStr}. Expected YYYYMMDD.`);
}
if (timeStr.length !== 6) {
throw new Error(`Invalid time format: ${timeStr}. Expected hhmmss.`);
}

const year = dateStr.slice(0, 4);
const month = dateStr.slice(4, 6);
const day = dateStr.slice(6, 8);

const hh = timeStr.slice(0, 2);
const mm = timeStr.slice(2, 4);
const ss = timeStr.slice(4, 6);

return `${year}-${month}-${day}T${hh}:${mm}:${ss}00`;
}

function createQueryStringOfOutlook(event: CalendarEvent) {
const dateTimes = parseDateTimes(event);

const startDateTime = `${formatDateTimeForOutlook(dateTimes.startDate, dateTimes.startTime)}`;
const endDateTime = `${formatDateTimeForOutlook(dateTimes.endDate, dateTimes.endTime)}`;

const params = {
text: encodeURIComponent(event.title),
startdt: startDateTime,
enddt: endDateTime,
body: encodeURIComponent(event.details),
location: encodeURIComponent(event.location),
};
return `?subject=${params.text}&startdt=${params.startdt}&enddt=${params.enddt}&body=${params.body}&location=${params.location}`;
}
10 changes: 10 additions & 0 deletions extensions/ai-text-to-calendar/src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { CalendarEvent } from "../ai-text-to-calendar";

export function parseDateTimes(event: CalendarEvent) {
const startDate = event.start_date.replace(/\D/g, "");
const startTime = event.start_time.replace(/\D/g, "");
const endDate = event.end_date.replace(/\D/g, "");
const endTime = event.end_time.replace(/\D/g, "");

return { startDate, startTime, endDate, endTime };
}
Loading