Feat: show patterns in space

This commit is contained in:
Harshith Mullapudi 2025-08-26 23:32:17 +05:30
parent 1bae793675
commit 6588e36037
25 changed files with 539 additions and 95 deletions

View File

@ -62,9 +62,8 @@ export function NewSpaceDialog({
setName("");
editor?.commands.clearContent(true);
onOpenChange(false);
onSuccess?.();
}
}, [fetcher.data, fetcher.state, editor, onOpenChange, onSuccess]);
}, [fetcher.data, fetcher.state, editor, onOpenChange]);
return (
<Dialog open={open} onOpenChange={onOpenChange}>

View File

@ -27,7 +27,7 @@ export function SpaceFactCard({ fact }: SpaceFactCardProps) {
<div className="flex w-full items-center px-5 pr-2">
<div
className={cn(
"group-hover:bg-grayAlpha-100 flex min-w-[0px] shrink grow items-start gap-2 rounded-md px-4",
"group-hover:bg-grayAlpha-100 flex min-w-[0px] shrink grow items-start gap-2 rounded-md px-3",
)}
>
<div
@ -36,7 +36,7 @@ export function SpaceFactCard({ fact }: SpaceFactCardProps) {
)}
>
<div className="flex w-full items-center justify-between gap-4">
<div className="inline-flex min-h-[24px] min-w-[0px] shrink cursor-pointer items-center justify-start">
<div className="inline-flex min-h-[24px] min-w-[0px] shrink items-center justify-start">
<div className={cn("truncate text-left")}>{displayText}</div>
</div>
<div className="text-muted-foreground flex shrink-0 items-center justify-end gap-2 text-xs">

View File

@ -16,8 +16,8 @@ import {
AlertDialogHeader,
AlertDialogTitle,
} from "../ui/alert-dialog";
import { useState } from "react";
import { useFetcher } from "@remix-run/react";
import { useEffect, useState } from "react";
import { useFetcher, useNavigate } from "@remix-run/react";
import { EditSpaceDialog } from "./edit-space-dialog.client";
interface SpaceOptionsProps {
@ -32,20 +32,28 @@ export const SpaceOptions = ({ id, name, description }: SpaceOptionsProps) => {
const [editDialogOpen, setEditDialogOpen] = useState(false);
const deleteFetcher = useFetcher();
const resetFetcher = useFetcher();
const navigate = useNavigate();
const handleDelete = () => {
deleteFetcher.submit({
deleteFetcher.submit(null, {
method: "DELETE",
action: `/api/v1/space/${id}`,
action: `/api/v1/spaces/${id}`,
encType: "application/json",
});
setDeleteDialogOpen(false);
};
useEffect(() => {
if (deleteFetcher.state === "idle" && deleteFetcher.data) {
navigate("/home/space");
}
}, [deleteFetcher.state, deleteFetcher.data, navigate]);
const handleReset = () => {
resetFetcher.submit({
resetFetcher.submit(null, {
method: "POST",
action: `/api/v1/space/${id}/reset`,
action: `/api/v1/spaces/${id}/reset`,
encType: "application/json",
});
setResetSpace(false);
@ -111,8 +119,8 @@ export const SpaceOptions = ({ id, name, description }: SpaceOptionsProps) => {
<AlertDialogHeader>
<AlertDialogTitle>Delete space</AlertDialogTitle>
<AlertDialogDescription>
Are you sure you want to reset this space? This action cannot be
undone.
Are you sure you want to reset this space? This is create
categorise all facts again in this space
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>

View File

@ -0,0 +1,101 @@
import { Calendar } from "lucide-react";
import { Badge } from "~/components/ui/badge";
import { cn } from "~/lib/utils";
import { type SpacePattern } from "@prisma/client";
import { useState } from "react";
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "../ui/dialog";
import { Button } from "../ui";
import { useFetcher } from "@remix-run/react";
interface SpacePatternCardProps {
pattern: SpacePattern;
}
export function SpacePatternCard({ pattern }: SpacePatternCardProps) {
const [dialog, setDialog] = useState(false);
const fetcher = useFetcher();
const displayText = pattern.summary;
const handleAction = (actionType: "add" | "delete") => {
fetcher.submit(
{
actionType,
patternId: pattern.id,
},
{ method: "POST" }
);
setDialog(false);
};
return (
<>
<div className="group flex w-full items-center px-2 pr-2">
<div
className={cn(
"group-hover:bg-grayAlpha-100 flex min-w-[0px] shrink grow items-start gap-2 rounded-md px-3",
)}
onClick={() => setDialog(true)}
>
<div
className={cn(
"border-border flex w-full min-w-[0px] shrink flex-col border-b py-1",
)}
>
<div className="flex w-full items-center justify-between gap-6">
<div className="inline-flex min-h-[24px] min-w-[0px] shrink cursor-pointer items-center justify-start">
<div className={cn("truncate text-left")}>{displayText}</div>
</div>
<div className="text-muted-foreground flex shrink-0 items-center justify-end gap-2 text-xs">
<Badge variant="secondary" className="rounded text-xs">
{pattern.type}
</Badge>
<Badge variant="secondary" className="rounded text-xs">
{pattern.name}
</Badge>
</div>
</div>
</div>
</div>
</div>
<Dialog open={dialog} onOpenChange={setDialog}>
<DialogContent className="max-w-md overflow-auto p-4">
<DialogHeader>
<DialogTitle>Pattern</DialogTitle>
</DialogHeader>
<div className="flex flex-col gap-2">
<div className="flex gap-2">
<Badge variant="secondary" className="rounded text-xs">
{pattern.type}
</Badge>
<Badge variant="secondary" className="rounded text-xs">
{pattern.name}
</Badge>
</div>
<p>{displayText}</p>
<div className="flex justify-end">
<div className="flex gap-2">
<Button
variant="ghost"
onClick={() => handleAction("delete")}
disabled={fetcher.state === "submitting"}
>
Delete
</Button>
<Button
variant="secondary"
onClick={() => handleAction("add")}
disabled={fetcher.state === "submitting"}
>
Add
</Button>
</div>
</div>
</div>
</DialogContent>
</Dialog>
</>
);
}

View File

@ -0,0 +1,139 @@
import { useEffect, useRef } from "react";
import {
InfiniteLoader,
AutoSizer,
CellMeasurer,
CellMeasurerCache,
type Index,
type ListRowProps,
} from "react-virtualized";
import { Database } from "lucide-react";
import { Card, CardContent } from "~/components/ui/card";
import { ScrollManagedList } from "../virtualized-list";
import { type SpacePattern } from "@prisma/client";
import { SpacePatternCard } from "./space-pattern-card";
interface SpacePatternListProps {
patterns: any[];
hasMore: boolean;
loadMore: () => void;
isLoading: boolean;
height?: number;
}
function PatternItemRenderer(
props: ListRowProps,
patterns: SpacePattern[],
cache: CellMeasurerCache,
) {
const { index, key, style, parent } = props;
const pattern = patterns[index];
return (
<CellMeasurer
key={key}
cache={cache}
columnIndex={0}
parent={parent}
rowIndex={index}
>
<div key={key} style={style} className="pb-2">
<SpacePatternCard pattern={pattern} />
</div>
</CellMeasurer>
);
}
export function SpacePatternList({
patterns,
hasMore,
loadMore,
isLoading,
}: SpacePatternListProps) {
// Create a CellMeasurerCache instance using useRef to prevent recreation
const cacheRef = useRef<CellMeasurerCache | null>(null);
if (!cacheRef.current) {
cacheRef.current = new CellMeasurerCache({
defaultHeight: 200, // Default row height for fact cards
fixedWidth: true, // Rows have fixed width but dynamic height
});
}
const cache = cacheRef.current;
useEffect(() => {
cache.clearAll();
}, [patterns, cache]);
if (patterns.length === 0 && !isLoading) {
return (
<Card className="bg-background-2 w-full">
<CardContent className="bg-background-2 flex w-full items-center justify-center py-16">
<div className="text-center">
<Database className="text-muted-foreground mx-auto mb-4 h-12 w-12" />
<h3 className="mb-2 text-lg font-semibold">No patterns found</h3>
<p className="text-muted-foreground">
This space doesn't contain any patterns yet.
</p>
</div>
</CardContent>
</Card>
);
}
const isRowLoaded = ({ index }: { index: number }) => {
return !!patterns[index];
};
const loadMoreRows = async () => {
if (hasMore) {
return loadMore();
}
return false;
};
const rowRenderer = (props: ListRowProps) => {
return PatternItemRenderer(props, patterns, cache);
};
const rowHeight = ({ index }: Index) => {
return cache.getHeight(index, 0);
};
const itemCount = hasMore ? patterns.length + 1 : patterns.length;
return (
<div className="h-full grow overflow-hidden rounded-lg">
<AutoSizer className="h-full">
{({ width, height: autoHeight }) => (
<InfiniteLoader
isRowLoaded={isRowLoaded}
loadMoreRows={loadMoreRows}
rowCount={itemCount}
threshold={5}
>
{({ onRowsRendered, registerChild }) => (
<ScrollManagedList
ref={registerChild}
className="h-auto overflow-auto"
height={autoHeight}
width={width}
rowCount={itemCount}
rowHeight={rowHeight}
onRowsRendered={onRowsRendered}
rowRenderer={rowRenderer}
deferredMeasurementCache={cache}
overscanRowCount={10}
/>
)}
</InfiniteLoader>
)}
</AutoSizer>
{isLoading && (
<div className="text-muted-foreground p-4 text-center text-sm">
Loading more patterns...
</div>
)}
</div>
);
}

View File

@ -1,24 +1,29 @@
import neo4j from "neo4j-driver";
import { type RawTriplet } from "~/components/graph/type";
import { logger } from "~/services/logger.service";
import { singleton } from "~/utils/singleton";
// Create a driver instance
const driver = neo4j.driver(
process.env.NEO4J_URI ?? "bolt://localhost:7687",
neo4j.auth.basic(
process.env.NEO4J_USERNAME as string,
process.env.NEO4J_PASSWORD as string,
),
{
maxConnectionPoolSize: 50,
logging: {
level: "info",
logger: (level, message) => {
logger.info(message);
// Create a singleton driver instance
const driver = singleton("neo4j", getDriver);
function getDriver() {
return neo4j.driver(
process.env.NEO4J_URI ?? "bolt://localhost:7687",
neo4j.auth.basic(
process.env.NEO4J_USERNAME as string,
process.env.NEO4J_PASSWORD as string,
),
{
maxConnectionPoolSize: 50,
logging: {
level: "info",
logger: (level, message) => {
logger.info(message);
},
},
},
},
);
);
}
let schemaInitialized = false;

View File

@ -1,5 +1,6 @@
import { type Workspace } from "@core/database";
import { prisma } from "~/db.server";
import { SpaceService } from "~/services/space.server";
interface CreateWorkspaceDto {
name: string;
@ -7,6 +8,18 @@ interface CreateWorkspaceDto {
userId: string;
}
const spaceService = new SpaceService();
const profileRule = `
Store the users stable, non-sensitive identity and preference facts that improve personalization across assistants. Facts must be long-lived (expected validity 3 months) and broadly useful across contexts (not app-specific).
Include (examples):
Preferred name, pronunciation, public handles (GitHub/Twitter/LinkedIn URLs), primary email domain
Timezone, locale, working hours, meeting preferences (async/sync bias, default duration)
Role, team, company, office location (city-level only), seniority
Tooling defaults (editor, ticketing system, repo host), keyboard layout, OS
Communication preferences (tone, brevity vs. detail, summary-first)
Exclude: secrets/credentials; one-off or short-term states; health/financial/political/religious/sexual data; precise home address; raw event logs; app-specific analytics; anything the user did not explicitly consent to share.`;
export async function createWorkspace(
input: CreateWorkspaceDto,
): Promise<Workspace> {
@ -25,6 +38,13 @@ export async function createWorkspace(
},
});
await spaceService.createSpace({
name: "Profile",
description: profileRule,
userId: input.userId,
workspaceId: workspace.id,
});
return workspace;
}

View File

@ -1,20 +1,21 @@
import { z } from "zod";
import { createActionApiRoute } from "~/services/routeBuilders/apiBuilder.server";
import {
createActionApiRoute,
createHybridActionApiRoute,
} from "~/services/routeBuilders/apiBuilder.server";
import { SpaceService } from "~/services/space.server";
import { json } from "@remix-run/node";
import { createSpace } from "~/services/graphModels/space";
import { createSpace, deleteSpace } from "~/services/graphModels/space";
import { prisma } from "~/db.server";
import { logger } from "~/services/logger.service";
import { triggerSpaceAssignment } from "~/trigger/spaces/space-assignment";
const spaceService = new SpaceService();
// Schema for space ID parameter
const SpaceParamsSchema = z.object({
spaceId: z.string(),
});
const { loader, action } = createActionApiRoute(
const { loader, action } = createHybridActionApiRoute(
{
params: SpaceParamsSchema,
allowJWT: true,
@ -38,7 +39,7 @@ const { loader, action } = createActionApiRoute(
}
// Get statements in the space
await spaceService.deleteSpace(spaceId, userId);
await deleteSpace(spaceId, userId);
await createSpace(
space.id,

View File

@ -7,6 +7,7 @@ import {
import { SpaceService } from "~/services/space.server";
import { json } from "@remix-run/node";
import { apiCors } from "~/utils/apiCors";
import { getSpace } from "~/trigger/utils/space-utils";
const spaceService = new SpaceService();
@ -20,10 +21,10 @@ const UpdateSpaceSchema = z.object({
name: z.string().optional(),
description: z.string().optional(),
});
const { action } = createHybridActionApiRoute(
{
params: SpaceParamsSchema,
body: UpdateSpaceSchema.optional(),
allowJWT: true,
authorization: {
action: "manage",
@ -61,6 +62,12 @@ const { action } = createHybridActionApiRoute(
if (request.method === "DELETE") {
try {
const space = await getSpace(spaceId);
if (space?.name.toLowerCase() === "profile") {
throw new Error("You can't delete Profile space");
}
// Delete space
await spaceService.deleteSpace(spaceId, userId);

View File

@ -1,5 +1,5 @@
import { z } from "zod";
import { useActionData } from "@remix-run/react";
import { useActionData, useLoaderData } from "@remix-run/react";
import {
type ActionFunctionArgs,
json,
@ -18,10 +18,14 @@ import {
import { Button } from "~/components/ui";
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 { rootPath } from "~/utils/pathBuilder";
import { createWorkspace } from "~/models/workspace.server";
import { createWorkspace, getWorkspaceByUser } from "~/models/workspace.server";
import { typedjson } from "remix-typedjson";
const schema = z.object({
@ -62,14 +66,17 @@ export async function action({ request }: ActionFunctionArgs) {
export const loader = async ({ request }: LoaderFunctionArgs) => {
const user = await requireUser(request);
const workspace = await getWorkspaceByUser(user.id);
return typedjson({
user,
workspace,
});
};
export default function ConfirmBasicDetails() {
const lastSubmission = useActionData<typeof action>();
const { workspace } = useLoaderData<typeof loader>();
const [form, fields] = useForm({
lastSubmission: lastSubmission as any,

View File

@ -6,7 +6,6 @@ import { SpaceService } from "~/services/space.server";
import { SpaceFactsFilters } from "~/components/spaces/space-facts-filters";
import { SpaceFactsList } from "~/components/spaces/space-facts-list";
import type { StatementNode } from "@core/types";
import { ClientOnly } from "remix-utils/client-only";
import { LoaderCircle } from "lucide-react";

View File

@ -72,21 +72,22 @@ function getStatusDisplay(status?: string | null) {
label: "Processing",
variant: "outline" as const,
icon: <Activity className="h-3 w-3" />,
className: "text-blue-600 bg-blue-50 rounded border-border",
className: "text-success-foreground bg-success rounded border-none",
};
case "pending":
return {
label: "Pending",
variant: "outline" as const,
icon: <Clock className="h-3 w-3" />,
className: "text-orange-600 border-orange-200 bg-orange-50",
className: "text-warning-foreground bg-warning rounded border-none",
};
case "error":
return {
label: "Error",
variant: "outline" as const,
icon: <AlertCircle className="h-3 w-3" />,
className: "text-destructive rounded border-border bg-destructive/10",
className:
"text-destructive-foreground rounded bg-destructive border-none",
};
default:
return null;

View File

@ -0,0 +1,105 @@
import { useLoaderData } from "@remix-run/react";
import {
type ActionFunctionArgs,
type LoaderFunctionArgs,
} from "@remix-run/server-runtime";
import { LoaderCircle } from "lucide-react";
import { ClientOnly } from "remix-utils/client-only";
import { SpacePatternList } from "~/components/spaces/space-pattern-list";
import { requireUserId, requireWorkpace } from "~/services/session.server";
import { SpacePattern } from "~/services/spacePattern.server";
import { addToQueue } from "~/lib/ingest.server";
import { redirect } from "@remix-run/node";
import { SpaceService } from "~/services/space.server";
export async function loader({ request, params }: LoaderFunctionArgs) {
const workspace = await requireWorkpace(request);
const spaceService = new SpacePattern();
const spaceId = params.spaceId as string;
const spacePatterns = await spaceService.getSpacePatternsForSpace(
spaceId,
workspace.id,
);
return {
spacePatterns: spacePatterns || [],
};
}
export async function action({ request, params }: ActionFunctionArgs) {
const workspace = await requireWorkpace(request);
const userId = await requireUserId(request);
const spaceService = new SpaceService();
const spacePatternService = new SpacePattern();
const spaceId = params.spaceId as string;
const formData = await request.formData();
const actionType = formData.get("actionType") as string;
const patternId = formData.get("patternId") as string;
if (actionType === "delete" || actionType === "add") {
// Get the space pattern to access its data
const spacePattern = await spacePatternService.getSpacePatternById(
patternId,
workspace.id,
);
if (!spacePattern) {
throw new Error("Space pattern not found");
}
// Get the space to access its name
const space = await spaceService.getSpace(spaceId, workspace.id);
if (!space) {
throw new Error("Space not found");
}
// Always delete the space pattern
await spacePatternService.deleteSpacePattern(patternId, workspace.id);
// If it's an "add" action, also trigger ingestion
if (actionType === "add") {
await addToQueue(
{
episodeBody: spacePattern.summary,
referenceTime: new Date().toISOString(),
metadata: {
pattern: spacePattern.name,
},
source: space.name,
spaceId: space.id,
},
userId,
);
}
}
return redirect(`/home/space/${spaceId}/patterns`);
}
export default function Patterns() {
const { spacePatterns } = useLoaderData<typeof loader>();
const loadMore = () => {
// TODO: Implement pagination
};
return (
<div className="flex h-full w-full flex-col pt-2">
<div className="flex h-[calc(100vh_-_140px)] w-full">
<ClientOnly
fallback={<LoaderCircle className="mr-2 h-4 w-4 animate-spin" />}
>
{() => (
<SpacePatternList
patterns={spacePatterns}
hasMore={false} // TODO: Implement real pagination
loadMore={loadMore}
isLoading={false}
/>
)}
</ClientOnly>
</div>
</div>
);
}

View File

@ -51,6 +51,12 @@ export default function Space() {
isActive: location.pathname.includes("/facts"),
onClick: () => navigate(`/home/space/${space.id}/facts`),
},
{
label: "Patterns",
value: "patterns",
isActive: location.pathname.includes("/patterns"),
onClick: () => navigate(`/home/space/${space.id}/patterns`),
},
]}
actionsNode={
<ClientOnly

View File

@ -28,7 +28,7 @@ export default function Spaces() {
const handleNewSpaceSuccess = () => {
// Refresh the page to show the new space
setShowNewSpaceDialog(false);
// setShowNewSpaceDialog(false);
};
return (
@ -52,11 +52,15 @@ export default function Spaces() {
fallback={<LoaderCircle className="mr-2 h-4 w-4 animate-spin" />}
>
{() => (
<NewSpaceDialog
open={showNewSpaceDialog}
onOpenChange={setShowNewSpaceDialog}
onSuccess={handleNewSpaceSuccess}
/>
<>
{showNewSpaceDialog && (
<NewSpaceDialog
open={showNewSpaceDialog}
onOpenChange={setShowNewSpaceDialog}
onSuccess={handleNewSpaceSuccess}
/>
)}
</>
)}
</ClientOnly>
)}

View File

@ -16,8 +16,8 @@ const client = singleton(
new EmailClient({
transport: buildTransportOptions(),
imagesBaseUrl: env.APP_ORIGIN,
from: env.FROM_EMAIL ?? "team@core.heysol.ai",
replyTo: env.REPLY_TO_EMAIL ?? "help@core.heysol.ai",
from: env.FROM_EMAIL ?? "Harshith <harshith@tegon.ai>",
replyTo: env.REPLY_TO_EMAIL ?? "harshith@tegon.ai",
}),
);
@ -55,26 +55,6 @@ function buildTransportOptions(): MailTransportOptions {
}
}
export async function sendMagicLinkEmail(options: any): Promise<void> {
logger.debug("Sending magic link email", {
emailAddress: options.emailAddress,
});
try {
return await client.send({
email: "magic_link",
to: options.emailAddress,
magicLink: options.magicLink,
});
} catch (error) {
logger.error("Error sending magic link email", {
error: JSON.stringify(error),
});
throw error;
}
}
export async function sendPlainTextEmail(options: SendPlainTextOptions) {
return client.sendPlainText(options);
}

View File

@ -56,6 +56,7 @@ export class SpaceService {
name: params.name.trim(),
description: params.description?.trim(),
workspaceId: params.workspaceId,
status: "pending",
},
});

View File

@ -0,0 +1,64 @@
import { prisma } from "~/db.server";
export class SpacePattern {
async getSpacePatternsForSpace(spaceId: string, workspaceId: string) {
const space = await prisma.space.findUnique({
where: {
id: spaceId,
workspaceId,
},
});
if (!space) {
throw new Error("No space found");
}
const spacePatterns = await prisma.spacePattern.findMany({
where: {
spaceId: space?.id,
deleted: null,
},
});
return spacePatterns;
}
async getSpacePatternById(patternId: string, workspaceId: string) {
const spacePattern = await prisma.spacePattern.findFirst({
where: {
id: patternId,
space: {
workspaceId,
},
},
});
return spacePattern;
}
async deleteSpacePattern(patternId: string, workspaceId: string) {
const spacePattern = await prisma.spacePattern.findFirst({
where: {
id: patternId,
space: {
workspaceId,
},
},
});
if (!spacePattern) {
throw new Error("Space pattern not found");
}
await prisma.spacePattern.update({
where: {
id: patternId,
},
data: {
deleted: new Date(),
},
});
return spacePattern;
}
}

View File

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

View File

@ -4,10 +4,7 @@ import { z } from "zod";
import { openai } from "@ai-sdk/openai";
import { logger } from "~/services/logger.service";
import {
deletePersonalAccessToken,
getOrCreatePersonalAccessToken,
} from "../utils/utils";
import { getOrCreatePersonalAccessToken } from "../utils/utils";
import axios from "axios";
export const ExtensionSearchBodyRequest = z.object({
@ -109,12 +106,9 @@ If no relevant information is found, provide a brief statement indicating that.`
finalText = finalText + chunk;
}
await deletePersonalAccessToken(pat.id);
return finalText;
} catch (error) {
logger.error(`SearchMemoryAgent error: ${error}`);
await deletePersonalAccessToken(pat.id);
return `Context related to: ${userInput}. Looking for relevant background information, previous discussions, and related concepts that would help provide a comprehensive answer.`;
}

View File

@ -553,6 +553,7 @@ async function processBatchAI(
maxRetries: 3,
timeoutMs: 600000, // 10 minutes timeout
});
logger.info(`Batch AI job created: ${batchId}`, {
userId,
mode,

View File

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

View File

@ -3,7 +3,7 @@ import { Footer } from "./components/Footer";
import { Image } from "./components/Image";
import { anchor, container, h1, main, paragraphLight } from "./components/styles";
export default function Email({ magicLink }: { magicLink: string }) {
export default function MagicLinkEmail({ magicLink }: { magicLink: string }) {
return (
<Html>
<Head />

View File

@ -1,14 +1,23 @@
import { Body, Head, Html, Link, Preview, Section, Text } from "@react-email/components";
import { Footer } from "./components/Footer";
import { anchor, bullets, footerItalic, main, paragraphLight } from "./components/styles";
import { z } from "zod";
export default function Email({ name }: { name?: string }) {
export const WelcomeEmailSchema = z.object({
email: z.literal("welcome"),
orgName: z.string(),
inviterName: z.string().optional(),
inviterEmail: z.string(),
inviteLink: z.string().url(),
});
export function WelcomeEmail({ orgName }: { orgName?: string }) {
return (
<Html>
<Head />
<Preview>Welcome to C.O.R.E. - Your Personal AI Assistant</Preview>
<Body style={main}>
<Text style={paragraphLight}>Hey {name ?? "there"},</Text>
<Text style={paragraphLight}>Hey {orgName ?? "there"},</Text>
<Text style={paragraphLight}>Welcome to C.O.R.E., your new personal AI assistant!</Text>
<Text style={paragraphLight}>
I'm excited to help you streamline your daily tasks, boost your productivity, and make

View File

@ -4,10 +4,9 @@ import { z } from "zod";
import { setGlobalBasePath } from "../emails/components/BasePath";
import InviteEmail, { InviteEmailSchema } from "../emails/invite";
import MagicLinkEmail from "../emails/magic-link";
import WelcomeEmail from "../emails/welcome";
import { WelcomeEmail, WelcomeEmailSchema } from "../emails/welcome";
import { constructMailTransport, MailTransport, MailTransportOptions } from "./transports";
import MagicLinkEmail from "../emails/magic-link";
export { type MailTransportOptions };
@ -17,7 +16,7 @@ export const DeliverEmailSchema = z
email: z.literal("magic_link"),
magicLink: z.string().url(),
}),
InviteEmailSchema,
WelcomeEmailSchema,
])
.and(z.object({ to: z.string() }));
@ -74,13 +73,14 @@ export class EmailClient {
switch (data.email) {
case "magic_link":
return {
subject: "Magic sign-in link for C.O.R.E.",
subject: "Magic sign-in link for Trigger.dev",
component: <MagicLinkEmail magicLink={data.magicLink} />,
};
case "invite":
case "welcome":
return {
subject: `You've been invited to join ${data.orgName} on C.O.R.E.`,
component: <InviteEmail {...data} />,
component: <WelcomeEmail {...data} />,
};
}
}