mirror of
https://github.com/eliasstepanik/core.git
synced 2026-01-11 09:08:28 +00:00
Feat: show patterns in space
This commit is contained in:
parent
1bae793675
commit
6588e36037
@ -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}>
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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>
|
||||
|
||||
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 { 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;
|
||||
|
||||
|
||||
@ -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<Workspace> {
|
||||
@ -25,6 +38,13 @@ export async function createWorkspace(
|
||||
},
|
||||
});
|
||||
|
||||
await spaceService.createSpace({
|
||||
name: "Profile",
|
||||
description: profileRule,
|
||||
userId: input.userId,
|
||||
workspaceId: workspace.id,
|
||||
});
|
||||
|
||||
return workspace;
|
||||
}
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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";
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
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"),
|
||||
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
|
||||
|
||||
@ -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>
|
||||
)}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -56,6 +56,7 @@ export class SpaceService {
|
||||
name: params.name.trim(),
|
||||
description: params.description?.trim(),
|
||||
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 {
|
||||
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);
|
||||
}
|
||||
},
|
||||
|
||||
@ -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.`;
|
||||
}
|
||||
|
||||
@ -553,6 +553,7 @@ async function processBatchAI(
|
||||
maxRetries: 3,
|
||||
timeoutMs: 600000, // 10 minutes timeout
|
||||
});
|
||||
|
||||
logger.info(`Batch AI job created: ${batchId}`, {
|
||||
userId,
|
||||
mode,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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 />
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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} />,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user