diff --git a/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/Citation/index.jsx b/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/Citation/index.jsx
index 2fa65bb9f..940d69cd5 100644
--- a/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/Citation/index.jsx
+++ b/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/Citation/index.jsx
@@ -12,6 +12,9 @@ import {
LinkSimple,
GitlabLogo,
} from "@phosphor-icons/react";
+import GmailLogo from "@/pages/Admin/Agents/GMailSkillPanel/gmail.png";
+import GoogleCalendarLogo from "@/pages/Admin/Agents/GoogleCalendarSkillPanel/google-calendar.png";
+import OutlookLogo from "@/pages/Admin/Agents/OutlookSkillPanel/outlook.png";
import { toPercentString } from "@/utils/numbers";
import { useTranslation } from "react-i18next";
import { useSourcesSidebar } from "../../SourcesSidebar";
@@ -28,6 +31,14 @@ const CIRCLE_ICONS = {
paperlessNgx: FileText,
};
+const CIRCLE_IMAGES = {
+ gmailThread: GmailLogo,
+ gmailAttachment: GmailLogo,
+ googleCalendar: GoogleCalendarLogo,
+ outlookThread: OutlookLogo,
+ outlookAttachment: OutlookLogo,
+};
+
/**
* Renders a circle with a source type icon inside, or a favicon if URL is provided.
* @param {"file"|"link"|"youtube"|"github"|"gitlab"|"confluence"|"drupalwiki"|"obsidian"|"paperlessNgx"} props.type
@@ -42,6 +53,7 @@ export function SourceTypeCircle({
url = null,
}) {
const Icon = CIRCLE_ICONS[type] || CIRCLE_ICONS.file;
+ const customImage = CIRCLE_IMAGES[type];
const [imgError, setImgError] = useState(false);
let faviconUrl = null;
@@ -71,6 +83,13 @@ export function SourceTypeCircle({
className="object-cover"
onError={() => setImgError(true)}
/>
+ ) : customImage ? (
+
) : (
)}
@@ -262,6 +281,11 @@ const supportedSources = [
"youtube://",
"obsidian://",
"paperless-ngx://",
+ "gmail-thread://",
+ "gmail-attachment://",
+ "google-calendar://",
+ "outlook-thread://",
+ "outlook-attachment://",
];
/**
@@ -342,6 +366,30 @@ export function parseChunkSource({ title = "", chunks = [] }) {
icon = "paperlessNgx";
break;
+ case "gmail-thread://":
+ text = title;
+ icon = "gmailThread";
+ break;
+ case "gmail-attachment://":
+ text = title;
+ icon = "gmailAttachment";
+ break;
+
+ case "google-calendar://":
+ text = title;
+ icon = "googleCalendar";
+ break;
+
+ case "outlook-thread://":
+ text = title;
+ icon = "outlookThread";
+ break;
+
+ case "outlook-attachment://":
+ text = title;
+ icon = "outlookAttachment";
+ break;
+
default:
text = url.host + url.pathname;
icon = "link";
diff --git a/server/utils/agents/aibitat/plugins/gmail/search/gmail-read-thread.js b/server/utils/agents/aibitat/plugins/gmail/search/gmail-read-thread.js
index c03225b2e..05cdcc328 100644
--- a/server/utils/agents/aibitat/plugins/gmail/search/gmail-read-thread.js
+++ b/server/utils/agents/aibitat/plugins/gmail/search/gmail-read-thread.js
@@ -87,6 +87,14 @@ module.exports.GmailReadThread = {
`${this.caller}: Successfully read thread with ${thread.messageCount} messages`
);
+ this.super.addCitation?.({
+ id: `gmail-thread-${thread.id}`,
+ title: thread.subject,
+ text: messagesFormatted,
+ chunkSource: `gmail-thread://${thread.permalink}`,
+ score: null,
+ });
+
return (
`Thread: "${thread.subject}"\n` +
`Thread ID: ${thread.id}\n` +
diff --git a/server/utils/agents/aibitat/plugins/google-calendar/events/gcal-get-event.js b/server/utils/agents/aibitat/plugins/google-calendar/events/gcal-get-event.js
index 833d5d505..3aa3a3fec 100644
--- a/server/utils/agents/aibitat/plugins/google-calendar/events/gcal-get-event.js
+++ b/server/utils/agents/aibitat/plugins/google-calendar/events/gcal-get-event.js
@@ -80,7 +80,7 @@ module.exports.GCalGetEvent = {
.join("\n")
: " (none)";
- return (
+ const eventDetails =
`Event Details:\n` +
`Title: ${event.title}\n` +
`Event ID: ${event.eventId}\n` +
@@ -93,8 +93,16 @@ module.exports.GCalGetEvent = {
`Owned by me: ${event.isOwnedByMe ? "Yes" : "No"}\n` +
`Guests:\n${guestList}\n` +
`Created: ${new Date(event.dateCreated).toLocaleString()}\n` +
- `Last Updated: ${new Date(event.lastUpdated).toLocaleString()}`
- );
+ `Last Updated: ${new Date(event.lastUpdated).toLocaleString()}`;
+
+ this.super.addCitation?.({
+ id: `google-calendar-${event.eventId}`,
+ title: event.title,
+ text: eventDetails,
+ chunkSource: `google-calendar://${event.eventId}`,
+ score: null,
+ });
+ return eventDetails;
} catch (e) {
this.super.handlerProps.log(`gcal-get-event error: ${e.message}`);
this.super.introspect(`Error: ${e.message}`);
diff --git a/server/utils/agents/aibitat/plugins/google-calendar/events/gcal-get-events-for-day.js b/server/utils/agents/aibitat/plugins/google-calendar/events/gcal-get-events-for-day.js
index 9b312b9e5..d76c46e37 100644
--- a/server/utils/agents/aibitat/plugins/google-calendar/events/gcal-get-events-for-day.js
+++ b/server/utils/agents/aibitat/plugins/google-calendar/events/gcal-get-events-for-day.js
@@ -65,34 +65,42 @@ module.exports.GCalGetEventsForDay = {
`${this.caller}: Found ${eventCount} event(s) for ${date}`
);
- if (eventCount === 0) {
- return `No events scheduled for ${date}.`;
- }
+ if (eventCount === 0) return `No events scheduled for ${date}.`;
- const summary = events
- .map((event, i) => {
- let timeStr;
- if (event.isAllDayEvent) {
- timeStr = "All day";
- } else {
- const start = new Date(event.startTime).toLocaleTimeString(
- [],
- { hour: "2-digit", minute: "2-digit" }
- );
- const end = new Date(event.endTime).toLocaleTimeString([], {
- hour: "2-digit",
- minute: "2-digit",
- });
- timeStr = `${start} - ${end}`;
- }
- return (
- `${i + 1}. "${event.title}" (${timeStr})\n` +
- ` ID: ${event.eventId}` +
- (event.location ? `\n Location: ${event.location}` : "")
+ const summaries = [];
+ const citations = [];
+ events.forEach((event, i) => {
+ let timeStr;
+ if (event.isAllDayEvent) {
+ timeStr = "All day";
+ } else {
+ const start = new Date(event.startTime).toLocaleTimeString(
+ [],
+ { hour: "2-digit", minute: "2-digit" }
);
- })
- .join("\n\n");
+ const end = new Date(event.endTime).toLocaleTimeString([], {
+ hour: "2-digit",
+ minute: "2-digit",
+ });
+ timeStr = `${start} - ${end}`;
+ }
+ const eventDetails =
+ `${i + 1}. "${event.title}" (${timeStr})\n` +
+ ` ID: ${event.eventId}` +
+ (event.location ? `\n Location: ${event.location}` : "");
+ summaries.push(eventDetails);
+ citations.push({
+ id: `google-calendar-${event.eventId}`,
+ title: event.title,
+ text: eventDetails,
+ chunkSource: `google-calendar://${event.eventId}`,
+ score: null,
+ });
+ });
+
+ const summary = summaries.join("\n\n");
+ citations.forEach((c) => this.super.addCitation?.(c));
return `Events for ${date} (${eventCount} total):\n\n${summary}`;
} catch (e) {
this.super.handlerProps.log(
diff --git a/server/utils/agents/aibitat/plugins/google-calendar/events/gcal-get-events.js b/server/utils/agents/aibitat/plugins/google-calendar/events/gcal-get-events.js
index b6b9fa387..d633bd091 100644
--- a/server/utils/agents/aibitat/plugins/google-calendar/events/gcal-get-events.js
+++ b/server/utils/agents/aibitat/plugins/google-calendar/events/gcal-get-events.js
@@ -98,22 +98,44 @@ module.exports.GCalGetEvents = {
return `No events found between ${startDate} and ${endDate}${query ? ` matching "${query}"` : ""}.`;
}
- const summary = events
- .map((event, i) => {
- let timeStr;
- if (event.isAllDayEvent) {
- timeStr = `All day (${new Date(event.startDate).toLocaleDateString()})`;
- } else {
- timeStr = `${new Date(event.startTime).toLocaleString()} - ${new Date(event.endTime).toLocaleString()}`;
- }
- return (
- `${i + 1}. "${event.title}"\n` +
- ` ${timeStr}\n` +
- ` ID: ${event.eventId}` +
- (event.location ? `\n Location: ${event.location}` : "")
- );
- })
- .join("\n\n");
+ const summaries = [];
+ const citations = [];
+ events.forEach((event, i) => {
+ let timeStr;
+ if (event.isAllDayEvent) {
+ timeStr = `All day (${new Date(event.startDate).toLocaleDateString()})`;
+ } else {
+ const startTime = new Date(event.startTime);
+ const endTime = new Date(event.endTime);
+ const dateStr = startTime.toLocaleDateString();
+ const startTimeStr = startTime.toLocaleTimeString([], {
+ hour: "2-digit",
+ minute: "2-digit",
+ });
+ const endTimeStr = endTime.toLocaleTimeString([], {
+ hour: "2-digit",
+ minute: "2-digit",
+ });
+ timeStr = `${dateStr} ${startTimeStr} - ${endTimeStr}`;
+ }
+ const eventDetails =
+ `${i + 1}. "${event.title}"\n` +
+ ` ${timeStr}\n` +
+ ` ID: ${event.eventId}` +
+ (event.location ? `\n Location: ${event.location}` : "");
+
+ summaries.push(eventDetails);
+ citations.push({
+ id: `google-calendar-${event.eventId}`,
+ title: event.title,
+ text: eventDetails,
+ chunkSource: `google-calendar://${event.eventId}`,
+ score: null,
+ });
+ });
+
+ const summary = summaries.join("\n\n");
+ citations.forEach((c) => this.super.addCitation?.(c));
let response = `Found ${totalEvents} event(s)`;
if (returnedEvents < totalEvents) {
diff --git a/server/utils/agents/aibitat/plugins/google-calendar/events/gcal-get-upcoming-events.js b/server/utils/agents/aibitat/plugins/google-calendar/events/gcal-get-upcoming-events.js
index c49656eda..a0b4ea013 100644
--- a/server/utils/agents/aibitat/plugins/google-calendar/events/gcal-get-upcoming-events.js
+++ b/server/utils/agents/aibitat/plugins/google-calendar/events/gcal-get-upcoming-events.js
@@ -186,33 +186,44 @@ module.exports.GCalGetUpcomingEvents = {
return `No events scheduled for ${label}${query ? ` matching "${query}"` : ""}.`;
}
- const summary = events
- .map((event, i) => {
- let timeStr;
- if (event.isAllDayEvent) {
- timeStr = `All day (${new Date(event.startDate).toLocaleDateString()})`;
- } else {
- const startTime = new Date(event.startTime);
- const endTime = new Date(event.endTime);
- const dateStr = startTime.toLocaleDateString();
- const startTimeStr = startTime.toLocaleTimeString([], {
- hour: "2-digit",
- minute: "2-digit",
- });
- const endTimeStr = endTime.toLocaleTimeString([], {
- hour: "2-digit",
- minute: "2-digit",
- });
- timeStr = `${dateStr} ${startTimeStr} - ${endTimeStr}`;
- }
- return (
- `${i + 1}. "${event.title}"\n` +
- ` ${timeStr}\n` +
- ` ID: ${event.eventId}` +
- (event.location ? `\n Location: ${event.location}` : "")
- );
- })
- .join("\n\n");
+ const summaries = [];
+ const citations = [];
+ events.forEach((event, i) => {
+ let timeStr;
+ if (event.isAllDayEvent) {
+ timeStr = `All day (${new Date(event.startDate).toLocaleDateString()})`;
+ } else {
+ const startTime = new Date(event.startTime);
+ const endTime = new Date(event.endTime);
+ const dateStr = startTime.toLocaleDateString();
+ const startTimeStr = startTime.toLocaleTimeString([], {
+ hour: "2-digit",
+ minute: "2-digit",
+ });
+ const endTimeStr = endTime.toLocaleTimeString([], {
+ hour: "2-digit",
+ minute: "2-digit",
+ });
+ timeStr = `${dateStr} ${startTimeStr} - ${endTimeStr}`;
+ }
+ const eventDetails =
+ `${i + 1}. "${event.title}"\n` +
+ ` ${timeStr}\n` +
+ ` ID: ${event.eventId}` +
+ (event.location ? `\n Location: ${event.location}` : "");
+
+ summaries.push(eventDetails);
+ citations.push({
+ id: `google-calendar-${event.eventId}`,
+ title: event.title,
+ text: eventDetails,
+ chunkSource: `google-calendar://${event.eventId}`,
+ score: null,
+ });
+ });
+
+ const summary = summaries.join("\n\n");
+ citations.forEach((c) => this.super.addCitation?.(c));
let response = `Events for ${label}`;
if (returnedEvents < totalEvents) {
diff --git a/server/utils/agents/aibitat/plugins/outlook/search/outlook-read-thread.js b/server/utils/agents/aibitat/plugins/outlook/search/outlook-read-thread.js
index 9ea34be02..14e92dc8d 100644
--- a/server/utils/agents/aibitat/plugins/outlook/search/outlook-read-thread.js
+++ b/server/utils/agents/aibitat/plugins/outlook/search/outlook-read-thread.js
@@ -95,6 +95,15 @@ module.exports.OutlookReadThread = {
`${this.caller}: Successfully read thread with ${thread.messageCount} messages`
);
+ // Report citation for the thread (without attachments)
+ this.super.addCitation?.({
+ id: `outlook-thread-${thread.conversationId}`,
+ title: thread.subject,
+ text: `Subject: "${thread.subject}"\n\n${messagesFormatted}`,
+ chunkSource: `outlook-thread://${this._generatePermalink(thread.conversationId)}`,
+ score: null,
+ });
+
return (
`Thread: "${thread.subject}"\n` +
`Conversation ID: ${thread.conversationId}\n` +
@@ -107,6 +116,14 @@ module.exports.OutlookReadThread = {
return handleSkillError(this, "outlook-read-thread", e);
}
},
+ _generatePermalink: function (conversationId) {
+ if (!conversationId) return null;
+ let encodedId = encodeURIComponent(conversationId);
+ // For outlook, this needs to be specifically encoded
+ // as the webpage does not respect it like traditional URL encoding
+ encodedId = encodedId.replace(/-/g, "%2F");
+ return `https://outlook.live.com/mail/inbox/id/${encodedId}`;
+ },
});
},
};