fix: emails are not sent on welcome

This commit is contained in:
Harshith Mullapudi 2025-08-27 22:44:32 +05:30
parent 26a2d04ca9
commit d062df14aa
14 changed files with 166 additions and 101 deletions

View File

@ -22,7 +22,7 @@ export function SpacePatternCard({ pattern }: SpacePatternCardProps) {
actionType, actionType,
patternId: pattern.id, patternId: pattern.id,
}, },
{ method: "POST" } { method: "POST" },
); );
setDialog(false); setDialog(false);
}; };
@ -89,7 +89,7 @@ export function SpacePatternCard({ pattern }: SpacePatternCardProps) {
onClick={() => handleAction("add")} onClick={() => handleAction("add")}
disabled={fetcher.state === "submitting"} disabled={fetcher.state === "submitting"}
> >
Add Add to memory
</Button> </Button>
</div> </div>
</div> </div>

View File

@ -1,5 +1,6 @@
import { type Workspace } from "@core/database"; import { type Workspace } from "@core/database";
import { prisma } from "~/db.server"; import { prisma } from "~/db.server";
import { sendEmail } from "~/services/email.server";
import { SpaceService } from "~/services/space.server"; import { SpaceService } from "~/services/space.server";
interface CreateWorkspaceDto { interface CreateWorkspaceDto {
@ -31,7 +32,7 @@ export async function createWorkspace(
}, },
}); });
await prisma.user.update({ const user = await prisma.user.update({
where: { id: input.userId }, where: { id: input.userId },
data: { data: {
confirmedBasicDetails: true, confirmedBasicDetails: true,
@ -45,6 +46,8 @@ export async function createWorkspace(
workspaceId: workspace.id, workspaceId: workspace.id,
}); });
await sendEmail({ email: "welcome", to: user.email });
return workspace; return workspace;
} }

View File

@ -1,5 +1,8 @@
import { z } from "zod"; import { z } from "zod";
import { createActionApiRoute } from "~/services/routeBuilders/apiBuilder.server"; import {
createActionApiRoute,
createHybridActionApiRoute,
} from "~/services/routeBuilders/apiBuilder.server";
import { SearchService } from "~/services/search.server"; import { SearchService } from "~/services/search.server";
import { json } from "@remix-run/node"; import { json } from "@remix-run/node";
@ -19,7 +22,7 @@ export const SearchBodyRequest = z.object({
}); });
const searchService = new SearchService(); const searchService = new SearchService();
const { action, loader } = createActionApiRoute( const { action, loader } = createHybridActionApiRoute(
{ {
body: SearchBodyRequest, body: SearchBodyRequest,
allowJWT: true, allowJWT: true,

View File

@ -1,5 +1,5 @@
import { z } from "zod"; import { z } from "zod";
import { useActionData, useLoaderData } from "@remix-run/react"; import { useActionData } from "@remix-run/react";
import { import {
type ActionFunctionArgs, type ActionFunctionArgs,
json, json,
@ -17,12 +17,7 @@ import {
} from "~/components/ui/card"; } from "~/components/ui/card";
import { Button } from "~/components/ui"; import { Button } from "~/components/ui";
import { Input } from "~/components/ui/input"; import { Input } from "~/components/ui/input";
import { useState } from "react"; import { requireUser, requireUserId } from "~/services/session.server";
import {
requireUser,
requireUserId,
requireWorkpace,
} from "~/services/session.server";
import { redirectWithSuccessMessage } from "~/models/message.server"; import { redirectWithSuccessMessage } from "~/models/message.server";
import { rootPath } from "~/utils/pathBuilder"; import { rootPath } from "~/utils/pathBuilder";
import { createWorkspace, getWorkspaceByUser } from "~/models/workspace.server"; import { createWorkspace, getWorkspaceByUser } from "~/models/workspace.server";
@ -76,7 +71,6 @@ export const loader = async ({ request }: LoaderFunctionArgs) => {
export default function ConfirmBasicDetails() { export default function ConfirmBasicDetails() {
const lastSubmission = useActionData<typeof action>(); const lastSubmission = useActionData<typeof action>();
const { workspace } = useLoaderData<typeof loader>();
const [form, fields] = useForm({ const [form, fields] = useForm({
lastSubmission: lastSubmission as any, lastSubmission: lastSubmission as any,

View File

@ -16,8 +16,8 @@ const client = singleton(
new EmailClient({ new EmailClient({
transport: buildTransportOptions(), transport: buildTransportOptions(),
imagesBaseUrl: env.APP_ORIGIN, imagesBaseUrl: env.APP_ORIGIN,
from: env.FROM_EMAIL ?? "Harshith <harshith@tegon.ai>", from: env.FROM_EMAIL ?? "Manik <manik@poozle.dev>",
replyTo: env.REPLY_TO_EMAIL ?? "harshith@tegon.ai", replyTo: env.REPLY_TO_EMAIL ?? "manik@poozle.dev",
}), }),
); );
@ -85,5 +85,9 @@ export async function scheduleEmail(
) {} ) {}
export async function sendEmail(data: DeliverEmail) { export async function sendEmail(data: DeliverEmail) {
return client.send(data); try {
return client.send(data);
} catch (e) {
logger.error(`Error: ${e}`);
}
} }

View File

@ -6,6 +6,7 @@ import { MCP } from "../utils/mcp";
import { type HistoryStep } from "../utils/types"; import { type HistoryStep } from "../utils/types";
import { import {
createConversationHistoryForAgent, createConversationHistoryForAgent,
deletePersonalAccessToken,
getCreditsForUser, getCreditsForUser,
getPreviousExecutionHistory, getPreviousExecutionHistory,
init, init,
@ -120,9 +121,15 @@ export const chat = task({
); );
usageCredits && (await updateUserCredits(usageCredits, 1)); usageCredits && (await updateUserCredits(usageCredits, 1));
if (init?.tokenId) {
await deletePersonalAccessToken(init.tokenId);
}
} catch (e) { } catch (e) {
await updateConversationStatus("failed", payload.conversationId); await updateConversationStatus("failed", payload.conversationId);
if (init?.tokenId) {
await deletePersonalAccessToken(init.tokenId);
}
throw new Error(e as string); throw new Error(e as string);
} }
}, },

View File

@ -4,8 +4,12 @@ import { z } from "zod";
import { openai } from "@ai-sdk/openai"; import { openai } from "@ai-sdk/openai";
import { logger } from "~/services/logger.service"; import { logger } from "~/services/logger.service";
import { getOrCreatePersonalAccessToken } from "../utils/utils"; import {
deletePersonalAccessToken,
getOrCreatePersonalAccessToken,
} from "../utils/utils";
import axios from "axios"; import axios from "axios";
import { nanoid } from "nanoid";
export const ExtensionSearchBodyRequest = z.object({ export const ExtensionSearchBodyRequest = z.object({
userInput: z.string().min(1, "User input is required"), userInput: z.string().min(1, "User input is required"),
@ -24,8 +28,10 @@ export const extensionSearch = task({
const { userInput, userId, context } = const { userInput, userId, context } =
ExtensionSearchBodyRequest.parse(body); ExtensionSearchBodyRequest.parse(body);
const randomKeyName = `extensionSearch_${nanoid(10)}`;
const pat = await getOrCreatePersonalAccessToken({ const pat = await getOrCreatePersonalAccessToken({
name: "extensionSearch", name: randomKeyName,
userId: userId as string, userId: userId as string,
}); });
@ -106,8 +112,12 @@ If no relevant information is found, provide a brief statement indicating that.`
finalText = finalText + chunk; finalText = finalText + chunk;
} }
await deletePersonalAccessToken(pat?.id);
return finalText; return finalText;
} catch (error) { } catch (error) {
await deletePersonalAccessToken(pat?.id);
logger.error(`SearchMemoryAgent error: ${error}`); logger.error(`SearchMemoryAgent error: ${error}`);
return `Context related to: ${userInput}. Looking for relevant background information, previous discussions, and related concepts that would help provide a comprehensive answer.`; return `Context related to: ${userInput}. Looking for relevant background information, previous discussions, and related concepts that would help provide a comprehensive answer.`;

View File

@ -170,7 +170,7 @@ export const init = async ({ payload }: { payload: InitChatPayload }) => {
return { conversation, conversationHistory }; return { conversation, conversationHistory };
} }
const randomKeyName = `chat`; const randomKeyName = `chat_${nanoid(10)}`;
const pat = await getOrCreatePersonalAccessToken({ const pat = await getOrCreatePersonalAccessToken({
name: randomKeyName, name: randomKeyName,
userId: workspace.userId as string, userId: workspace.userId as string,

View File

@ -1,16 +1,16 @@
import { Hr, Link, Text } from "@react-email/components"; import { Hr, Text } from "@react-email/components";
import React from "react"; import React from "react";
import { footer, footerAnchor, hr } from "./styles"; import { footer, hr, paragraphLight } from "./styles";
export function Footer() { export function Footer() {
return ( return (
<> <>
<Hr style={hr} /> <Hr style={hr} />
<Text style={paragraphLight}>happy building your digital brain!</Text>
<Text style={footer}> <Text style={footer}>
©Sol.ai the Core team P.S Questions?
<Link style={footerAnchor} href="https://core.heysol.ai/"> <br />
C.O.R.E Just hit reply - we're here to help.
</Link>
</Text> </Text>
</> </>
); );

View File

@ -8,10 +8,7 @@ export const h1 = {
padding: "0", padding: "0",
}; };
export const main = { export const main = {};
backgroundColor: "#15171A",
padding: "0 20px",
};
export const container = { export const container = {
backgroundColor: "#15171A", backgroundColor: "#15171A",
@ -30,29 +27,42 @@ export const hr = {
}; };
export const paragraph = { export const paragraph = {
color: "#878C99", color: "black",
fontFamily: fontFamily:
'-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Ubuntu,sans-serif', '-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Ubuntu,sans-serif',
fontSize: "16px", fontSize: "14px",
lineHeight: "24px", lineHeight: "24px",
textAlign: "left" as const, textAlign: "left" as const,
}; };
export const paragraphLight = { export const paragraphLight = {
color: "#D7D9DD", color: "black",
fontFamily: fontFamily:
'-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Ubuntu,sans-serif', '-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Ubuntu,sans-serif',
fontSize: "16px", fontSize: "14px",
lineHeight: "24px", lineHeight: "24px",
textAlign: "left" as const, textAlign: "left" as const,
margin: 0,
};
export const heading = {
color: "black",
fontFamily:
'-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Ubuntu,sans-serif',
fontSize: "14px",
lineHeight: "24px",
textAlign: "left" as const,
fontWeight: "bold",
margin: 0,
marginTop: "20px",
}; };
export const paragraphTight = { export const paragraphTight = {
color: "#D7D9DD", color: "black",
fontFamily: fontFamily:
'-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Ubuntu,sans-serif', '-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Ubuntu,sans-serif',
fontSize: "16px", fontSize: "14px",
lineHeight: "16px", lineHeight: "14px",
textAlign: "left" as const, textAlign: "left" as const,
}; };
@ -60,18 +70,19 @@ export const bullets = {
color: "#D7D9DD", color: "#D7D9DD",
fontFamily: fontFamily:
'-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Ubuntu,sans-serif', '-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Ubuntu,sans-serif',
fontSize: "16px", fontSize: "14px",
lineHeight: "24px", lineHeight: "24px",
textAlign: "left" as const, textAlign: "left" as const,
margin: "0", margin: "0",
}; };
export const anchor = { export const anchor = {
color: "#826DFF", marginRight: "3px",
fontFamily: fontFamily:
"-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif", "-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif",
fontSize: "16px", fontSize: "14px",
textDecoration: "underline", textDecoration: "underline",
color: "black",
}; };
export const button = { export const button = {
@ -80,7 +91,7 @@ export const button = {
color: "#D7D9DD", color: "#D7D9DD",
fontFamily: fontFamily:
'-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Ubuntu,sans-serif', '-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Ubuntu,sans-serif',
fontSize: "16px", fontSize: "14px",
fontWeight: "bold", fontWeight: "bold",
textDecoration: "none", textDecoration: "none",
textAlign: "center" as const, textAlign: "center" as const,
@ -92,7 +103,7 @@ export const footer = {
fontFamily: fontFamily:
'-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Ubuntu,sans-serif', '-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Ubuntu,sans-serif',
fontSize: "12px", fontSize: "12px",
lineHeight: "16px", lineHeight: "14px",
}; };
export const footerItalic = { export const footerItalic = {
@ -101,7 +112,7 @@ export const footerItalic = {
fontFamily: fontFamily:
'-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Ubuntu,sans-serif', '-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Ubuntu,sans-serif',
fontSize: "12px", fontSize: "12px",
lineHeight: "16px", lineHeight: "14px",
}; };
export const footerAnchor = { export const footerAnchor = {

View File

@ -1,59 +1,86 @@
import { Body, Head, Html, Link, Preview, Section, Text } from "@react-email/components"; import { Body, Head, Html, Img, Link, Preview, Text } from "@react-email/components";
import { Footer } from "./components/Footer"; import { Footer } from "./components/Footer";
import { anchor, bullets, footerItalic, main, paragraphLight } from "./components/styles"; import { anchor, heading, main, paragraphLight } from "./components/styles";
import { z } from "zod"; import { z } from "zod";
export const WelcomeEmailSchema = z.object({ export const WelcomeEmailSchema = z.object({
email: z.literal("welcome"), email: z.literal("welcome"),
orgName: z.string(),
inviterName: z.string().optional(),
inviterEmail: z.string(),
inviteLink: z.string().url(),
}); });
export function WelcomeEmail({ orgName }: { orgName?: string }) { export default function WelcomeEmail() {
return ( return (
<Html> <Html>
<Head /> <Head />
<Preview>Welcome to C.O.R.E. - Your Personal AI Assistant</Preview> <Preview>building your digital brain</Preview>
<Body style={main}> <Body style={main}>
<Text style={paragraphLight}>Hey {orgName ?? "there"},</Text> <Text style={paragraphLight}>hi there,</Text>
<Text style={paragraphLight}>Welcome to C.O.R.E., your new personal AI assistant!</Text> <Text
<Text style={paragraphLight}> style={{
I'm excited to help you streamline your daily tasks, boost your productivity, and make ...paragraphLight,
your work life easier. C.O.R.E. is designed to be intuitive and powerful, adapting to your marginTop: "10px",
unique needs and preferences. }}
</Text> >
<Text style={paragraphLight}> <Link style={anchor} href="https://x.com/manikagg01">
To get started, you can{" "} Manik
<Link style={anchor} href="https://core.heysol.ai/home">
visit your dashboard
</Link>{" "}
where you'll find all the features and capabilities at your disposal. Whether it's
managing your schedule, handling communications, or automating repetitive tasks, I'm here
to help.
</Text>
<Text style={paragraphLight}>
If you have any questions or need assistance, don't hesitate to reach out. You can:{"\n"}
Ask me directly through the chat interface{"\n"}{" "}
<Link style={anchor} href="https://core.heysol.ai/support">
Visit our support center
</Link> </Link>
{"\n"} Join our{" "} from core here. welcome to core. when i first tried core memory, two actions made it click
<Link style={anchor} href="https://discord.gg/heysol"> for me. each came down to the same thing: understanding how I can add relevant context
Discord community about everything that matters to me in core memory and recall it wherever I want.
</Link>{" "}
to connect with other users and our team
</Text> </Text>
<Text style={heading}>core mcp</Text>
<Text style={paragraphLight}>
seamlessly add your code context from cursor/claude-code, project context from linear, or
brainstorming sessions from claude desktop via mcp. solve context loss problems across ai
tools with persistent, cross-session memory. add this url and get started
</Text>
<Link
style={{
...anchor,
marginTop: "10px",
marginBottom: "10px",
}}
>
https://core.heysol.ai/api/v1/mcp?source='Your Coding Agent'
</Link>
<Img
alt="Claude"
style={{
marginLeft: "auto",
marginRight: "auto",
width: "100%",
borderRadius: "2%",
marginTop: "10px",
}}
src="https://integrations.heysol.ai/core-claude.gif"
/>
<Text style={heading}>browser extension</Text>
<Text style={paragraphLight}>
recall relevant context from core memory in chatgpt, grok, and gemini. save conversations
and content from chatgpt, grok, gemini, twitter, youtube, blog posts, and any webpage
directly into your Core memory with simple text selection.
</Text>
<Img
alt="Claude"
style={{
marginLeft: "auto",
marginRight: "auto",
width: "100%",
borderRadius: "2%",
marginTop: "10px",
}}
src="https://integrations.heysol.ai/core-extension.gif"
/>
<Text style={paragraphLight}>Looking forward to being your trusted assistant!</Text> <Text style={heading}>need real-time, human help to get started? </Text>
<Text style={paragraphLight}>
<Text style={bullets}>Best regards,</Text> - join our discord community & get direct help from our team + over 100+ enthusiasts using
<Text style={bullets}>C.O.R.E.</Text> Core memory
<Text style={paragraphLight}>Your AI Assistant</Text> </Text>
<Text style={footerItalic}> <Text style={paragraphLight}>
You can customize your notification preferences anytime in your account settings. - We are open-source us on our repo -{" "}
<Link style={anchor} href="https://github.com/RedPlanetHQ/core">
https://github.com/RedPlanetHQ/core
</Link>
</Text> </Text>
<Footer /> <Footer />
</Body> </Body>

View File

@ -4,7 +4,7 @@ import { z } from "zod";
import { setGlobalBasePath } from "../emails/components/BasePath"; import { setGlobalBasePath } from "../emails/components/BasePath";
import { WelcomeEmail, WelcomeEmailSchema } from "../emails/welcome"; import WelcomeEmail, { WelcomeEmailSchema } from "../emails/welcome";
import { constructMailTransport, MailTransport, MailTransportOptions } from "./transports"; import { constructMailTransport, MailTransport, MailTransportOptions } from "./transports";
import MagicLinkEmail from "../emails/magic-link"; import MagicLinkEmail from "../emails/magic-link";
@ -73,14 +73,14 @@ export class EmailClient {
switch (data.email) { switch (data.email) {
case "magic_link": case "magic_link":
return { return {
subject: "Magic sign-in link for Trigger.dev", subject: "Magic sign-in link for Core",
component: <MagicLinkEmail magicLink={data.magicLink} />, component: <MagicLinkEmail magicLink={data.magicLink} />,
}; };
case "welcome": case "welcome":
return { return {
subject: `You've been invited to join ${data.orgName} on C.O.R.E.`, subject: `You've been invited to join on C.O.R.E.`,
component: <WelcomeEmail {...data} />, component: <WelcomeEmail />,
}; };
} }
} }

View File

@ -2,20 +2,20 @@ import { EmailError, MailMessage, MailTransport, PlainTextMailMessage } from "./
import { Resend } from "resend"; import { Resend } from "resend";
export type ResendMailTransportOptions = { export type ResendMailTransportOptions = {
type: 'resend', type: "resend";
config: { config: {
apiKey?: string apiKey?: string;
} };
} };
export class ResendMailTransport implements MailTransport { export class ResendMailTransport implements MailTransport {
#client: Resend; #client: Resend;
constructor(options: ResendMailTransportOptions) { constructor(options: ResendMailTransportOptions) {
this.#client = new Resend(options.config.apiKey) this.#client = new Resend(options.config.apiKey);
} }
async send({to, from, replyTo, subject, react}: MailMessage): Promise<void> { async send({ to, from, replyTo, subject, react }: MailMessage): Promise<void> {
const result = await this.#client.emails.send({ const result = await this.#client.emails.send({
from: from, from: from,
to, to,
@ -25,6 +25,7 @@ export class ResendMailTransport implements MailTransport {
}); });
if (result.error) { if (result.error) {
console.log(result);
console.error( console.error(
`Failed to send email to ${to}, ${subject}. Error ${result.error.name}: ${result.error.message}` `Failed to send email to ${to}, ${subject}. Error ${result.error.name}: ${result.error.message}`
); );
@ -32,7 +33,7 @@ export class ResendMailTransport implements MailTransport {
} }
} }
async sendPlainText({to, from, replyTo, subject, text}: PlainTextMailMessage): Promise<void> { async sendPlainText({ to, from, replyTo, subject, text }: PlainTextMailMessage): Promise<void> {
const result = await this.#client.emails.send({ const result = await this.#client.emails.send({
from: from, from: from,
to, to,

View File

@ -74,6 +74,11 @@
"TRIGGER_API_URL", "TRIGGER_API_URL",
"TRIGGER_SECRET_KEY", "TRIGGER_SECRET_KEY",
"EMBEDDING_MODEL", "EMBEDDING_MODEL",
"MODEL" "MODEL",
"COHERE_API_KEY",
"RESEND_API_KEY",
"FROM_EMAIL",
"REPLY_TO_EMAIL",
"EMAIL_TRANSPORT"
] ]
} }