From 6588e3603772857eb97eb41e1f40e0c150d87de6 Mon Sep 17 00:00:00 2001 From: Harshith Mullapudi Date: Tue, 26 Aug 2025 23:32:17 +0530 Subject: [PATCH] Feat: show patterns in space --- .../spaces/new-space-dialog.client.tsx | 3 +- .../app/components/spaces/space-fact-card.tsx | 4 +- .../app/components/spaces/space-options.tsx | 24 ++- .../components/spaces/space-pattern-card.tsx | 101 +++++++++++++ .../components/spaces/space-pattern-list.tsx | 139 ++++++++++++++++++ apps/webapp/app/lib/neo4j.server.ts | 35 +++-- apps/webapp/app/models/workspace.server.ts | 20 +++ .../routes/api.v1.spaces.$spaceId.reset.ts | 13 +- .../app/routes/api.v1.spaces.$spaceId.ts | 9 +- .../app/routes/confirm-basic-details.tsx | 13 +- .../app/routes/home.space.$spaceId.facts.tsx | 1 - .../routes/home.space.$spaceId.overview.tsx | 7 +- .../routes/home.space.$spaceId.patterns.tsx | 105 +++++++++++++ .../webapp/app/routes/home.space.$spaceId.tsx | 6 + apps/webapp/app/routes/home.space._index.tsx | 16 +- apps/webapp/app/services/email.server.ts | 24 +-- apps/webapp/app/services/space.server.ts | 1 + .../app/services/spacePattern.server.ts | 64 ++++++++ apps/webapp/app/trigger/chat/chat.ts | 9 +- apps/webapp/app/trigger/extension/search.ts | 8 +- .../app/trigger/spaces/space-assignment.ts | 1 + apps/webapp/app/trigger/utils/utils.ts | 2 +- packages/emails/emails/magic-link.tsx | 2 +- packages/emails/emails/welcome.tsx | 13 +- packages/emails/src/index.tsx | 14 +- 25 files changed, 539 insertions(+), 95 deletions(-) create mode 100644 apps/webapp/app/components/spaces/space-pattern-card.tsx create mode 100644 apps/webapp/app/components/spaces/space-pattern-list.tsx create mode 100644 apps/webapp/app/routes/home.space.$spaceId.patterns.tsx create mode 100644 apps/webapp/app/services/spacePattern.server.ts diff --git a/apps/webapp/app/components/spaces/new-space-dialog.client.tsx b/apps/webapp/app/components/spaces/new-space-dialog.client.tsx index 20e9270..36d3b98 100644 --- a/apps/webapp/app/components/spaces/new-space-dialog.client.tsx +++ b/apps/webapp/app/components/spaces/new-space-dialog.client.tsx @@ -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 ( diff --git a/apps/webapp/app/components/spaces/space-fact-card.tsx b/apps/webapp/app/components/spaces/space-fact-card.tsx index 2ec47f4..bb003df 100644 --- a/apps/webapp/app/components/spaces/space-fact-card.tsx +++ b/apps/webapp/app/components/spaces/space-fact-card.tsx @@ -27,7 +27,7 @@ export function SpaceFactCard({ fact }: SpaceFactCardProps) {
-
+
{displayText}
diff --git a/apps/webapp/app/components/spaces/space-options.tsx b/apps/webapp/app/components/spaces/space-options.tsx index c3dfcd0..d2170c3 100644 --- a/apps/webapp/app/components/spaces/space-options.tsx +++ b/apps/webapp/app/components/spaces/space-options.tsx @@ -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) => { Delete space - 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 diff --git a/apps/webapp/app/components/spaces/space-pattern-card.tsx b/apps/webapp/app/components/spaces/space-pattern-card.tsx new file mode 100644 index 0000000..e61201f --- /dev/null +++ b/apps/webapp/app/components/spaces/space-pattern-card.tsx @@ -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 ( + <> +
+
setDialog(true)} + > +
+
+
+
{displayText}
+
+
+ + {pattern.type} + + + {pattern.name} + +
+
+
+
+
+ + + + + Pattern + + +
+
+ + {pattern.type} + + + {pattern.name} + +
+

{displayText}

+ +
+
+ + +
+
+
+
+
+ + ); +} diff --git a/apps/webapp/app/components/spaces/space-pattern-list.tsx b/apps/webapp/app/components/spaces/space-pattern-list.tsx new file mode 100644 index 0000000..85ba6ef --- /dev/null +++ b/apps/webapp/app/components/spaces/space-pattern-list.tsx @@ -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 ( + +
+ +
+
+ ); +} + +export function SpacePatternList({ + patterns, + hasMore, + loadMore, + isLoading, +}: SpacePatternListProps) { + // Create a CellMeasurerCache instance using useRef to prevent recreation + const cacheRef = useRef(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 ( + + +
+ +

No patterns found

+

+ This space doesn't contain any patterns yet. +

+
+
+
+ ); + } + + 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 ( +
+ + {({ width, height: autoHeight }) => ( + + {({ onRowsRendered, registerChild }) => ( + + )} + + )} + + + {isLoading && ( +
+ Loading more patterns... +
+ )} +
+ ); +} diff --git a/apps/webapp/app/lib/neo4j.server.ts b/apps/webapp/app/lib/neo4j.server.ts index ed21625..22a498c 100644 --- a/apps/webapp/app/lib/neo4j.server.ts +++ b/apps/webapp/app/lib/neo4j.server.ts @@ -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; diff --git a/apps/webapp/app/models/workspace.server.ts b/apps/webapp/app/models/workspace.server.ts index 106e9d3..d9cb78d 100644 --- a/apps/webapp/app/models/workspace.server.ts +++ b/apps/webapp/app/models/workspace.server.ts @@ -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 user’s 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 { @@ -25,6 +38,13 @@ export async function createWorkspace( }, }); + await spaceService.createSpace({ + name: "Profile", + description: profileRule, + userId: input.userId, + workspaceId: workspace.id, + }); + return workspace; } diff --git a/apps/webapp/app/routes/api.v1.spaces.$spaceId.reset.ts b/apps/webapp/app/routes/api.v1.spaces.$spaceId.reset.ts index 42f8b1f..60e87ea 100644 --- a/apps/webapp/app/routes/api.v1.spaces.$spaceId.reset.ts +++ b/apps/webapp/app/routes/api.v1.spaces.$spaceId.reset.ts @@ -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, diff --git a/apps/webapp/app/routes/api.v1.spaces.$spaceId.ts b/apps/webapp/app/routes/api.v1.spaces.$spaceId.ts index ea72e2c..1998c5c 100644 --- a/apps/webapp/app/routes/api.v1.spaces.$spaceId.ts +++ b/apps/webapp/app/routes/api.v1.spaces.$spaceId.ts @@ -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); diff --git a/apps/webapp/app/routes/confirm-basic-details.tsx b/apps/webapp/app/routes/confirm-basic-details.tsx index 70b567a..2653034 100644 --- a/apps/webapp/app/routes/confirm-basic-details.tsx +++ b/apps/webapp/app/routes/confirm-basic-details.tsx @@ -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(); + const { workspace } = useLoaderData(); const [form, fields] = useForm({ lastSubmission: lastSubmission as any, diff --git a/apps/webapp/app/routes/home.space.$spaceId.facts.tsx b/apps/webapp/app/routes/home.space.$spaceId.facts.tsx index 981312b..ec420db 100644 --- a/apps/webapp/app/routes/home.space.$spaceId.facts.tsx +++ b/apps/webapp/app/routes/home.space.$spaceId.facts.tsx @@ -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"; diff --git a/apps/webapp/app/routes/home.space.$spaceId.overview.tsx b/apps/webapp/app/routes/home.space.$spaceId.overview.tsx index d4c50e3..95f5eba 100644 --- a/apps/webapp/app/routes/home.space.$spaceId.overview.tsx +++ b/apps/webapp/app/routes/home.space.$spaceId.overview.tsx @@ -72,21 +72,22 @@ function getStatusDisplay(status?: string | null) { label: "Processing", variant: "outline" as const, icon: , - 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: , - 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: , - className: "text-destructive rounded border-border bg-destructive/10", + className: + "text-destructive-foreground rounded bg-destructive border-none", }; default: return null; diff --git a/apps/webapp/app/routes/home.space.$spaceId.patterns.tsx b/apps/webapp/app/routes/home.space.$spaceId.patterns.tsx new file mode 100644 index 0000000..c1da349 --- /dev/null +++ b/apps/webapp/app/routes/home.space.$spaceId.patterns.tsx @@ -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(); + + const loadMore = () => { + // TODO: Implement pagination + }; + + return ( +
+
+ } + > + {() => ( + + )} + +
+
+ ); +} diff --git a/apps/webapp/app/routes/home.space.$spaceId.tsx b/apps/webapp/app/routes/home.space.$spaceId.tsx index 5ca4cb6..26daa97 100644 --- a/apps/webapp/app/routes/home.space.$spaceId.tsx +++ b/apps/webapp/app/routes/home.space.$spaceId.tsx @@ -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={ { // Refresh the page to show the new space - setShowNewSpaceDialog(false); + // setShowNewSpaceDialog(false); }; return ( @@ -52,11 +52,15 @@ export default function Spaces() { fallback={} > {() => ( - + <> + {showNewSpaceDialog && ( + + )} + )} )} diff --git a/apps/webapp/app/services/email.server.ts b/apps/webapp/app/services/email.server.ts index 45f3e57..e3dc47c 100644 --- a/apps/webapp/app/services/email.server.ts +++ b/apps/webapp/app/services/email.server.ts @@ -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 ", + replyTo: env.REPLY_TO_EMAIL ?? "harshith@tegon.ai", }), ); @@ -55,26 +55,6 @@ function buildTransportOptions(): MailTransportOptions { } } -export async function sendMagicLinkEmail(options: any): Promise { - 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); } diff --git a/apps/webapp/app/services/space.server.ts b/apps/webapp/app/services/space.server.ts index 2bd6b6e..447dd0f 100644 --- a/apps/webapp/app/services/space.server.ts +++ b/apps/webapp/app/services/space.server.ts @@ -56,6 +56,7 @@ export class SpaceService { name: params.name.trim(), description: params.description?.trim(), workspaceId: params.workspaceId, + status: "pending", }, }); diff --git a/apps/webapp/app/services/spacePattern.server.ts b/apps/webapp/app/services/spacePattern.server.ts new file mode 100644 index 0000000..3b2d702 --- /dev/null +++ b/apps/webapp/app/services/spacePattern.server.ts @@ -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; + } +} diff --git a/apps/webapp/app/trigger/chat/chat.ts b/apps/webapp/app/trigger/chat/chat.ts index f72b5eb..99de2fc 100644 --- a/apps/webapp/app/trigger/chat/chat.ts +++ b/apps/webapp/app/trigger/chat/chat.ts @@ -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); } }, diff --git a/apps/webapp/app/trigger/extension/search.ts b/apps/webapp/app/trigger/extension/search.ts index e9c56c1..a291d4f 100644 --- a/apps/webapp/app/trigger/extension/search.ts +++ b/apps/webapp/app/trigger/extension/search.ts @@ -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.`; } diff --git a/apps/webapp/app/trigger/spaces/space-assignment.ts b/apps/webapp/app/trigger/spaces/space-assignment.ts index 33391e5..579d0ac 100644 --- a/apps/webapp/app/trigger/spaces/space-assignment.ts +++ b/apps/webapp/app/trigger/spaces/space-assignment.ts @@ -553,6 +553,7 @@ async function processBatchAI( maxRetries: 3, timeoutMs: 600000, // 10 minutes timeout }); + logger.info(`Batch AI job created: ${batchId}`, { userId, mode, diff --git a/apps/webapp/app/trigger/utils/utils.ts b/apps/webapp/app/trigger/utils/utils.ts index 18a9699..da3279c 100644 --- a/apps/webapp/app/trigger/utils/utils.ts +++ b/apps/webapp/app/trigger/utils/utils.ts @@ -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, diff --git a/packages/emails/emails/magic-link.tsx b/packages/emails/emails/magic-link.tsx index b39b941..9cdd212 100644 --- a/packages/emails/emails/magic-link.tsx +++ b/packages/emails/emails/magic-link.tsx @@ -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 ( diff --git a/packages/emails/emails/welcome.tsx b/packages/emails/emails/welcome.tsx index ff637b8..abd2880 100644 --- a/packages/emails/emails/welcome.tsx +++ b/packages/emails/emails/welcome.tsx @@ -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 ( Welcome to C.O.R.E. - Your Personal AI Assistant - Hey {name ?? "there"}, + Hey {orgName ?? "there"}, Welcome to C.O.R.E., your new personal AI assistant! I'm excited to help you streamline your daily tasks, boost your productivity, and make diff --git a/packages/emails/src/index.tsx b/packages/emails/src/index.tsx index ec0cff2..6f7dc84 100644 --- a/packages/emails/src/index.tsx +++ b/packages/emails/src/index.tsx @@ -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: , }; - case "invite": + + case "welcome": return { subject: `You've been invited to join ${data.orgName} on C.O.R.E.`, - component: , + component: , }; } }