mirror of
https://github.com/eliasstepanik/core.git
synced 2026-01-21 21:38:29 +00:00
Feat: show patterns in space
This commit is contained in:
parent
ebee1e9331
commit
00cfea7e9e
@ -62,9 +62,8 @@ export function NewSpaceDialog({
|
|||||||
setName("");
|
setName("");
|
||||||
editor?.commands.clearContent(true);
|
editor?.commands.clearContent(true);
|
||||||
onOpenChange(false);
|
onOpenChange(false);
|
||||||
onSuccess?.();
|
|
||||||
}
|
}
|
||||||
}, [fetcher.data, fetcher.state, editor, onOpenChange, onSuccess]);
|
}, [fetcher.data, fetcher.state, editor, onOpenChange]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
|
|||||||
@ -27,7 +27,7 @@ export function SpaceFactCard({ fact }: SpaceFactCardProps) {
|
|||||||
<div className="flex w-full items-center px-5 pr-2">
|
<div className="flex w-full items-center px-5 pr-2">
|
||||||
<div
|
<div
|
||||||
className={cn(
|
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
|
<div
|
||||||
@ -36,7 +36,7 @@ export function SpaceFactCard({ fact }: SpaceFactCardProps) {
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex w-full items-center justify-between gap-4">
|
<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 className={cn("truncate text-left")}>{displayText}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-muted-foreground flex shrink-0 items-center justify-end gap-2 text-xs">
|
<div className="text-muted-foreground flex shrink-0 items-center justify-end gap-2 text-xs">
|
||||||
|
|||||||
@ -16,8 +16,8 @@ import {
|
|||||||
AlertDialogHeader,
|
AlertDialogHeader,
|
||||||
AlertDialogTitle,
|
AlertDialogTitle,
|
||||||
} from "../ui/alert-dialog";
|
} from "../ui/alert-dialog";
|
||||||
import { useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useFetcher } from "@remix-run/react";
|
import { useFetcher, useNavigate } from "@remix-run/react";
|
||||||
import { EditSpaceDialog } from "./edit-space-dialog.client";
|
import { EditSpaceDialog } from "./edit-space-dialog.client";
|
||||||
|
|
||||||
interface SpaceOptionsProps {
|
interface SpaceOptionsProps {
|
||||||
@ -32,20 +32,28 @@ export const SpaceOptions = ({ id, name, description }: SpaceOptionsProps) => {
|
|||||||
const [editDialogOpen, setEditDialogOpen] = useState(false);
|
const [editDialogOpen, setEditDialogOpen] = useState(false);
|
||||||
const deleteFetcher = useFetcher();
|
const deleteFetcher = useFetcher();
|
||||||
const resetFetcher = useFetcher();
|
const resetFetcher = useFetcher();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const handleDelete = () => {
|
const handleDelete = () => {
|
||||||
deleteFetcher.submit({
|
deleteFetcher.submit(null, {
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
action: `/api/v1/space/${id}`,
|
action: `/api/v1/spaces/${id}`,
|
||||||
encType: "application/json",
|
encType: "application/json",
|
||||||
});
|
});
|
||||||
|
|
||||||
setDeleteDialogOpen(false);
|
setDeleteDialogOpen(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (deleteFetcher.state === "idle" && deleteFetcher.data) {
|
||||||
|
navigate("/home/space");
|
||||||
|
}
|
||||||
|
}, [deleteFetcher.state, deleteFetcher.data, navigate]);
|
||||||
|
|
||||||
const handleReset = () => {
|
const handleReset = () => {
|
||||||
resetFetcher.submit({
|
resetFetcher.submit(null, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
action: `/api/v1/space/${id}/reset`,
|
action: `/api/v1/spaces/${id}/reset`,
|
||||||
encType: "application/json",
|
encType: "application/json",
|
||||||
});
|
});
|
||||||
setResetSpace(false);
|
setResetSpace(false);
|
||||||
@ -111,8 +119,8 @@ export const SpaceOptions = ({ id, name, description }: SpaceOptionsProps) => {
|
|||||||
<AlertDialogHeader>
|
<AlertDialogHeader>
|
||||||
<AlertDialogTitle>Delete space</AlertDialogTitle>
|
<AlertDialogTitle>Delete space</AlertDialogTitle>
|
||||||
<AlertDialogDescription>
|
<AlertDialogDescription>
|
||||||
Are you sure you want to reset this space? This action cannot be
|
Are you sure you want to reset this space? This is create
|
||||||
undone.
|
categorise all facts again in this space
|
||||||
</AlertDialogDescription>
|
</AlertDialogDescription>
|
||||||
</AlertDialogHeader>
|
</AlertDialogHeader>
|
||||||
<AlertDialogFooter>
|
<AlertDialogFooter>
|
||||||
|
|||||||
101
apps/webapp/app/components/spaces/space-pattern-card.tsx
Normal file
101
apps/webapp/app/components/spaces/space-pattern-card.tsx
Normal 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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
139
apps/webapp/app/components/spaces/space-pattern-list.tsx
Normal file
139
apps/webapp/app/components/spaces/space-pattern-list.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,24 +1,29 @@
|
|||||||
import neo4j from "neo4j-driver";
|
import neo4j from "neo4j-driver";
|
||||||
import { type RawTriplet } from "~/components/graph/type";
|
import { type RawTriplet } from "~/components/graph/type";
|
||||||
import { logger } from "~/services/logger.service";
|
import { logger } from "~/services/logger.service";
|
||||||
|
import { singleton } from "~/utils/singleton";
|
||||||
|
|
||||||
// Create a driver instance
|
// Create a singleton driver instance
|
||||||
const driver = neo4j.driver(
|
const driver = singleton("neo4j", getDriver);
|
||||||
process.env.NEO4J_URI ?? "bolt://localhost:7687",
|
|
||||||
neo4j.auth.basic(
|
function getDriver() {
|
||||||
process.env.NEO4J_USERNAME as string,
|
return neo4j.driver(
|
||||||
process.env.NEO4J_PASSWORD as string,
|
process.env.NEO4J_URI ?? "bolt://localhost:7687",
|
||||||
),
|
neo4j.auth.basic(
|
||||||
{
|
process.env.NEO4J_USERNAME as string,
|
||||||
maxConnectionPoolSize: 50,
|
process.env.NEO4J_PASSWORD as string,
|
||||||
logging: {
|
),
|
||||||
level: "info",
|
{
|
||||||
logger: (level, message) => {
|
maxConnectionPoolSize: 50,
|
||||||
logger.info(message);
|
logging: {
|
||||||
|
level: "info",
|
||||||
|
logger: (level, message) => {
|
||||||
|
logger.info(message);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
);
|
||||||
);
|
}
|
||||||
|
|
||||||
let schemaInitialized = false;
|
let schemaInitialized = false;
|
||||||
|
|
||||||
|
|||||||
@ -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 { SpaceService } from "~/services/space.server";
|
||||||
|
|
||||||
interface CreateWorkspaceDto {
|
interface CreateWorkspaceDto {
|
||||||
name: string;
|
name: string;
|
||||||
@ -7,6 +8,18 @@ interface CreateWorkspaceDto {
|
|||||||
userId: string;
|
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(
|
export async function createWorkspace(
|
||||||
input: CreateWorkspaceDto,
|
input: CreateWorkspaceDto,
|
||||||
): Promise<Workspace> {
|
): 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;
|
return workspace;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,20 +1,21 @@
|
|||||||
import { z } from "zod";
|
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 { SpaceService } from "~/services/space.server";
|
||||||
import { json } from "@remix-run/node";
|
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 { prisma } from "~/db.server";
|
||||||
import { logger } from "~/services/logger.service";
|
import { logger } from "~/services/logger.service";
|
||||||
import { triggerSpaceAssignment } from "~/trigger/spaces/space-assignment";
|
import { triggerSpaceAssignment } from "~/trigger/spaces/space-assignment";
|
||||||
|
|
||||||
const spaceService = new SpaceService();
|
|
||||||
|
|
||||||
// Schema for space ID parameter
|
// Schema for space ID parameter
|
||||||
const SpaceParamsSchema = z.object({
|
const SpaceParamsSchema = z.object({
|
||||||
spaceId: z.string(),
|
spaceId: z.string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const { loader, action } = createActionApiRoute(
|
const { loader, action } = createHybridActionApiRoute(
|
||||||
{
|
{
|
||||||
params: SpaceParamsSchema,
|
params: SpaceParamsSchema,
|
||||||
allowJWT: true,
|
allowJWT: true,
|
||||||
@ -38,7 +39,7 @@ const { loader, action } = createActionApiRoute(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get statements in the space
|
// Get statements in the space
|
||||||
await spaceService.deleteSpace(spaceId, userId);
|
await deleteSpace(spaceId, userId);
|
||||||
|
|
||||||
await createSpace(
|
await createSpace(
|
||||||
space.id,
|
space.id,
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import {
|
|||||||
import { SpaceService } from "~/services/space.server";
|
import { SpaceService } from "~/services/space.server";
|
||||||
import { json } from "@remix-run/node";
|
import { json } from "@remix-run/node";
|
||||||
import { apiCors } from "~/utils/apiCors";
|
import { apiCors } from "~/utils/apiCors";
|
||||||
|
import { getSpace } from "~/trigger/utils/space-utils";
|
||||||
|
|
||||||
const spaceService = new SpaceService();
|
const spaceService = new SpaceService();
|
||||||
|
|
||||||
@ -20,10 +21,10 @@ const UpdateSpaceSchema = z.object({
|
|||||||
name: z.string().optional(),
|
name: z.string().optional(),
|
||||||
description: z.string().optional(),
|
description: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const { action } = createHybridActionApiRoute(
|
const { action } = createHybridActionApiRoute(
|
||||||
{
|
{
|
||||||
params: SpaceParamsSchema,
|
params: SpaceParamsSchema,
|
||||||
body: UpdateSpaceSchema.optional(),
|
|
||||||
allowJWT: true,
|
allowJWT: true,
|
||||||
authorization: {
|
authorization: {
|
||||||
action: "manage",
|
action: "manage",
|
||||||
@ -61,6 +62,12 @@ const { action } = createHybridActionApiRoute(
|
|||||||
|
|
||||||
if (request.method === "DELETE") {
|
if (request.method === "DELETE") {
|
||||||
try {
|
try {
|
||||||
|
const space = await getSpace(spaceId);
|
||||||
|
|
||||||
|
if (space?.name.toLowerCase() === "profile") {
|
||||||
|
throw new Error("You can't delete Profile space");
|
||||||
|
}
|
||||||
|
|
||||||
// Delete space
|
// Delete space
|
||||||
await spaceService.deleteSpace(spaceId, userId);
|
await spaceService.deleteSpace(spaceId, userId);
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { useActionData } from "@remix-run/react";
|
import { useActionData, useLoaderData } from "@remix-run/react";
|
||||||
import {
|
import {
|
||||||
type ActionFunctionArgs,
|
type ActionFunctionArgs,
|
||||||
json,
|
json,
|
||||||
@ -18,10 +18,14 @@ import {
|
|||||||
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 { 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 } from "~/models/workspace.server";
|
import { createWorkspace, getWorkspaceByUser } from "~/models/workspace.server";
|
||||||
import { typedjson } from "remix-typedjson";
|
import { typedjson } from "remix-typedjson";
|
||||||
|
|
||||||
const schema = z.object({
|
const schema = z.object({
|
||||||
@ -62,14 +66,17 @@ export async function action({ request }: ActionFunctionArgs) {
|
|||||||
|
|
||||||
export const loader = async ({ request }: LoaderFunctionArgs) => {
|
export const loader = async ({ request }: LoaderFunctionArgs) => {
|
||||||
const user = await requireUser(request);
|
const user = await requireUser(request);
|
||||||
|
const workspace = await getWorkspaceByUser(user.id);
|
||||||
|
|
||||||
return typedjson({
|
return typedjson({
|
||||||
user,
|
user,
|
||||||
|
workspace,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
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,
|
||||||
|
|||||||
@ -6,7 +6,6 @@ import { SpaceService } from "~/services/space.server";
|
|||||||
import { SpaceFactsFilters } from "~/components/spaces/space-facts-filters";
|
import { SpaceFactsFilters } from "~/components/spaces/space-facts-filters";
|
||||||
import { SpaceFactsList } from "~/components/spaces/space-facts-list";
|
import { SpaceFactsList } from "~/components/spaces/space-facts-list";
|
||||||
|
|
||||||
import type { StatementNode } from "@core/types";
|
|
||||||
import { ClientOnly } from "remix-utils/client-only";
|
import { ClientOnly } from "remix-utils/client-only";
|
||||||
import { LoaderCircle } from "lucide-react";
|
import { LoaderCircle } from "lucide-react";
|
||||||
|
|
||||||
|
|||||||
@ -72,21 +72,22 @@ function getStatusDisplay(status?: string | null) {
|
|||||||
label: "Processing",
|
label: "Processing",
|
||||||
variant: "outline" as const,
|
variant: "outline" as const,
|
||||||
icon: <Activity className="h-3 w-3" />,
|
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":
|
case "pending":
|
||||||
return {
|
return {
|
||||||
label: "Pending",
|
label: "Pending",
|
||||||
variant: "outline" as const,
|
variant: "outline" as const,
|
||||||
icon: <Clock className="h-3 w-3" />,
|
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":
|
case "error":
|
||||||
return {
|
return {
|
||||||
label: "Error",
|
label: "Error",
|
||||||
variant: "outline" as const,
|
variant: "outline" as const,
|
||||||
icon: <AlertCircle className="h-3 w-3" />,
|
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:
|
default:
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
105
apps/webapp/app/routes/home.space.$spaceId.patterns.tsx
Normal file
105
apps/webapp/app/routes/home.space.$spaceId.patterns.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -51,6 +51,12 @@ export default function Space() {
|
|||||||
isActive: location.pathname.includes("/facts"),
|
isActive: location.pathname.includes("/facts"),
|
||||||
onClick: () => navigate(`/home/space/${space.id}/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={
|
actionsNode={
|
||||||
<ClientOnly
|
<ClientOnly
|
||||||
|
|||||||
@ -28,7 +28,7 @@ export default function Spaces() {
|
|||||||
|
|
||||||
const handleNewSpaceSuccess = () => {
|
const handleNewSpaceSuccess = () => {
|
||||||
// Refresh the page to show the new space
|
// Refresh the page to show the new space
|
||||||
setShowNewSpaceDialog(false);
|
// setShowNewSpaceDialog(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -52,11 +52,15 @@ export default function Spaces() {
|
|||||||
fallback={<LoaderCircle className="mr-2 h-4 w-4 animate-spin" />}
|
fallback={<LoaderCircle className="mr-2 h-4 w-4 animate-spin" />}
|
||||||
>
|
>
|
||||||
{() => (
|
{() => (
|
||||||
<NewSpaceDialog
|
<>
|
||||||
open={showNewSpaceDialog}
|
{showNewSpaceDialog && (
|
||||||
onOpenChange={setShowNewSpaceDialog}
|
<NewSpaceDialog
|
||||||
onSuccess={handleNewSpaceSuccess}
|
open={showNewSpaceDialog}
|
||||||
/>
|
onOpenChange={setShowNewSpaceDialog}
|
||||||
|
onSuccess={handleNewSpaceSuccess}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</ClientOnly>
|
</ClientOnly>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -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 ?? "team@core.heysol.ai",
|
from: env.FROM_EMAIL ?? "Harshith <harshith@tegon.ai>",
|
||||||
replyTo: env.REPLY_TO_EMAIL ?? "help@core.heysol.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) {
|
export async function sendPlainTextEmail(options: SendPlainTextOptions) {
|
||||||
return client.sendPlainText(options);
|
return client.sendPlainText(options);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -56,6 +56,7 @@ export class SpaceService {
|
|||||||
name: params.name.trim(),
|
name: params.name.trim(),
|
||||||
description: params.description?.trim(),
|
description: params.description?.trim(),
|
||||||
workspaceId: params.workspaceId,
|
workspaceId: params.workspaceId,
|
||||||
|
status: "pending",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
64
apps/webapp/app/services/spacePattern.server.ts
Normal file
64
apps/webapp/app/services/spacePattern.server.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -6,7 +6,6 @@ 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,
|
||||||
@ -121,15 +120,9 @@ 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);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@ -4,10 +4,7 @@ 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 {
|
import { getOrCreatePersonalAccessToken } from "../utils/utils";
|
||||||
deletePersonalAccessToken,
|
|
||||||
getOrCreatePersonalAccessToken,
|
|
||||||
} from "../utils/utils";
|
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
|
|
||||||
export const ExtensionSearchBodyRequest = z.object({
|
export const ExtensionSearchBodyRequest = z.object({
|
||||||
@ -109,12 +106,9 @@ 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) {
|
||||||
logger.error(`SearchMemoryAgent error: ${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.`;
|
return `Context related to: ${userInput}. Looking for relevant background information, previous discussions, and related concepts that would help provide a comprehensive answer.`;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -553,6 +553,7 @@ async function processBatchAI(
|
|||||||
maxRetries: 3,
|
maxRetries: 3,
|
||||||
timeoutMs: 600000, // 10 minutes timeout
|
timeoutMs: 600000, // 10 minutes timeout
|
||||||
});
|
});
|
||||||
|
|
||||||
logger.info(`Batch AI job created: ${batchId}`, {
|
logger.info(`Batch AI job created: ${batchId}`, {
|
||||||
userId,
|
userId,
|
||||||
mode,
|
mode,
|
||||||
|
|||||||
@ -170,7 +170,7 @@ export const init = async ({ payload }: { payload: InitChatPayload }) => {
|
|||||||
return { conversation, conversationHistory };
|
return { conversation, conversationHistory };
|
||||||
}
|
}
|
||||||
|
|
||||||
const randomKeyName = `chat_${nanoid(10)}`;
|
const randomKeyName = `chat`;
|
||||||
const pat = await getOrCreatePersonalAccessToken({
|
const pat = await getOrCreatePersonalAccessToken({
|
||||||
name: randomKeyName,
|
name: randomKeyName,
|
||||||
userId: workspace.userId as string,
|
userId: workspace.userId as string,
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { Footer } from "./components/Footer";
|
|||||||
import { Image } from "./components/Image";
|
import { Image } from "./components/Image";
|
||||||
import { anchor, container, h1, main, paragraphLight } from "./components/styles";
|
import { anchor, container, h1, main, paragraphLight } from "./components/styles";
|
||||||
|
|
||||||
export default function Email({ magicLink }: { magicLink: string }) {
|
export default function MagicLinkEmail({ magicLink }: { magicLink: string }) {
|
||||||
return (
|
return (
|
||||||
<Html>
|
<Html>
|
||||||
<Head />
|
<Head />
|
||||||
|
|||||||
@ -1,14 +1,23 @@
|
|||||||
import { Body, Head, Html, Link, Preview, Section, Text } from "@react-email/components";
|
import { Body, Head, Html, Link, Preview, Section, 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, 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 (
|
return (
|
||||||
<Html>
|
<Html>
|
||||||
<Head />
|
<Head />
|
||||||
<Preview>Welcome to C.O.R.E. - Your Personal AI Assistant</Preview>
|
<Preview>Welcome to C.O.R.E. - Your Personal AI Assistant</Preview>
|
||||||
<Body style={main}>
|
<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}>Welcome to C.O.R.E., your new personal AI assistant!</Text>
|
||||||
<Text style={paragraphLight}>
|
<Text style={paragraphLight}>
|
||||||
I'm excited to help you streamline your daily tasks, boost your productivity, and make
|
I'm excited to help you streamline your daily tasks, boost your productivity, and make
|
||||||
|
|||||||
@ -4,10 +4,9 @@ import { z } from "zod";
|
|||||||
|
|
||||||
import { setGlobalBasePath } from "../emails/components/BasePath";
|
import { setGlobalBasePath } from "../emails/components/BasePath";
|
||||||
|
|
||||||
import InviteEmail, { InviteEmailSchema } from "../emails/invite";
|
import { WelcomeEmail, WelcomeEmailSchema } from "../emails/welcome";
|
||||||
import MagicLinkEmail from "../emails/magic-link";
|
|
||||||
import WelcomeEmail from "../emails/welcome";
|
|
||||||
import { constructMailTransport, MailTransport, MailTransportOptions } from "./transports";
|
import { constructMailTransport, MailTransport, MailTransportOptions } from "./transports";
|
||||||
|
import MagicLinkEmail from "../emails/magic-link";
|
||||||
|
|
||||||
export { type MailTransportOptions };
|
export { type MailTransportOptions };
|
||||||
|
|
||||||
@ -17,7 +16,7 @@ export const DeliverEmailSchema = z
|
|||||||
email: z.literal("magic_link"),
|
email: z.literal("magic_link"),
|
||||||
magicLink: z.string().url(),
|
magicLink: z.string().url(),
|
||||||
}),
|
}),
|
||||||
InviteEmailSchema,
|
WelcomeEmailSchema,
|
||||||
])
|
])
|
||||||
.and(z.object({ to: z.string() }));
|
.and(z.object({ to: z.string() }));
|
||||||
|
|
||||||
@ -74,13 +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 C.O.R.E.",
|
subject: "Magic sign-in link for Trigger.dev",
|
||||||
component: <MagicLinkEmail magicLink={data.magicLink} />,
|
component: <MagicLinkEmail magicLink={data.magicLink} />,
|
||||||
};
|
};
|
||||||
case "invite":
|
|
||||||
|
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 ${data.orgName} on C.O.R.E.`,
|
||||||
component: <InviteEmail {...data} />,
|
component: <WelcomeEmail {...data} />,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user