mirror of
https://github.com/eliasstepanik/core.git
synced 2026-01-10 23:48:26 +00:00
feat: change spaces to episode based
This commit is contained in:
parent
fdc52ffc47
commit
0a75a68d1d
@ -0,0 +1,71 @@
|
||||
import { useState } from "react";
|
||||
import { FileText, Plus } from "lucide-react";
|
||||
import {
|
||||
CommandDialog,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from "../ui/command";
|
||||
import { AddMemoryDialog } from "./memory-dialog.client";
|
||||
import { AddDocumentDialog } from "./document-dialog";
|
||||
|
||||
interface AddMemoryCommandProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
}
|
||||
|
||||
export function AddMemoryCommand({
|
||||
open,
|
||||
onOpenChange,
|
||||
}: AddMemoryCommandProps) {
|
||||
const [showAddMemory, setShowAddMemory] = useState(false);
|
||||
const [showAddDocument, setShowAddDocument] = useState(false);
|
||||
|
||||
const handleAddMemory = () => {
|
||||
onOpenChange(false);
|
||||
setShowAddMemory(true);
|
||||
};
|
||||
|
||||
const handleAddDocument = () => {
|
||||
onOpenChange(false);
|
||||
setShowAddDocument(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Main Command Dialog */}
|
||||
<CommandDialog open={open} onOpenChange={onOpenChange}>
|
||||
<CommandInput placeholder="Search" className="py-1" />
|
||||
<CommandList>
|
||||
<CommandGroup heading="Add to Memory">
|
||||
<CommandItem
|
||||
onSelect={handleAddMemory}
|
||||
className="flex items-center gap-2 py-1"
|
||||
>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
<span>Add Memory</span>
|
||||
</CommandItem>
|
||||
<CommandItem
|
||||
onSelect={handleAddDocument}
|
||||
className="flex items-center gap-2 py-1"
|
||||
>
|
||||
<FileText className="mr-2 h-4 w-4" />
|
||||
<span>Add Document</span>
|
||||
</CommandItem>
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</CommandDialog>
|
||||
|
||||
{showAddMemory && (
|
||||
<AddMemoryDialog open={showAddMemory} onOpenChange={setShowAddMemory} />
|
||||
)}
|
||||
|
||||
{/* Add Document Dialog */}
|
||||
<AddDocumentDialog
|
||||
open={showAddDocument}
|
||||
onOpenChange={setShowAddDocument}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
27
apps/webapp/app/components/command-bar/document-dialog.tsx
Normal file
27
apps/webapp/app/components/command-bar/document-dialog.tsx
Normal file
@ -0,0 +1,27 @@
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "../ui/dialog";
|
||||
|
||||
interface AddDocumentDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
}
|
||||
|
||||
export function AddDocumentDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
}: AddDocumentDialogProps) {
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-[600px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Add Document</DialogTitle>
|
||||
</DialogHeader>
|
||||
{/* TODO: Add document content here */}
|
||||
<div className="border-border rounded-md border p-4">
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Document upload content goes here...
|
||||
</p>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,95 @@
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "../ui/dialog";
|
||||
import { useEditor, EditorContent } from "@tiptap/react";
|
||||
import {
|
||||
extensionsForConversation,
|
||||
getPlaceholder,
|
||||
} from "../conversation/editor-extensions";
|
||||
import { Button } from "../ui/button";
|
||||
import { SpaceDropdown } from "../spaces/space-dropdown";
|
||||
import React from "react";
|
||||
import { useFetcher } from "@remix-run/react";
|
||||
|
||||
interface AddMemoryDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
defaultSpaceId?: string;
|
||||
}
|
||||
|
||||
export function AddMemoryDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
defaultSpaceId,
|
||||
}: AddMemoryDialogProps) {
|
||||
const [spaceIds, setSpaceIds] = React.useState<string[]>(
|
||||
defaultSpaceId ? [defaultSpaceId] : [],
|
||||
);
|
||||
const fetcher = useFetcher();
|
||||
const editor = useEditor({
|
||||
extensions: [
|
||||
...extensionsForConversation,
|
||||
getPlaceholder("Write your memory here..."),
|
||||
],
|
||||
editorProps: {
|
||||
attributes: {
|
||||
class:
|
||||
"prose prose-sm focus:outline-none max-w-full min-h-[200px] p-4 py-0",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const handleAdd = async () => {
|
||||
const content = editor?.getText();
|
||||
if (!content?.trim()) return;
|
||||
|
||||
const payload = {
|
||||
episodeBody: content,
|
||||
referenceTime: new Date().toISOString(),
|
||||
spaceIds: spaceIds,
|
||||
source: "core",
|
||||
};
|
||||
|
||||
fetcher.submit(payload, {
|
||||
method: "POST",
|
||||
action: "/api/v1/add",
|
||||
encType: "application/json",
|
||||
});
|
||||
|
||||
// Clear editor and close dialog
|
||||
editor?.commands.clearContent();
|
||||
setSpaceIds([]);
|
||||
onOpenChange(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="pt-0 sm:max-w-[600px]">
|
||||
<div className="overflow-hidden rounded-md">
|
||||
<EditorContent editor={editor} />
|
||||
</div>
|
||||
<div className="flex justify-between gap-2 px-4 pb-4">
|
||||
<div>
|
||||
<SpaceDropdown
|
||||
episodeIds={[]}
|
||||
selectedSpaceIds={spaceIds}
|
||||
onSpaceChange={(spaceIds) => {
|
||||
setSpaceIds(spaceIds);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button variant="ghost" onClick={() => onOpenChange(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={handleAdd}
|
||||
isLoading={fetcher.state !== "idle"}
|
||||
>
|
||||
Add
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@ -9,6 +9,7 @@ import TableHeader from "@tiptap/extension-table-header";
|
||||
import TableRow from "@tiptap/extension-table-row";
|
||||
import { all, createLowlight } from "lowlight";
|
||||
import { mergeAttributes, type Extension } from "@tiptap/react";
|
||||
import { Markdown } from "tiptap-markdown";
|
||||
|
||||
// create a lowlight instance with all languages loaded
|
||||
export const lowlight = createLowlight(all);
|
||||
@ -136,4 +137,5 @@ export const extensionsForConversation = [
|
||||
CodeBlockLowlight.configure({
|
||||
lowlight,
|
||||
}),
|
||||
Markdown,
|
||||
];
|
||||
|
||||
@ -83,10 +83,14 @@ export const GraphClusteringVisualization = forwardRef<
|
||||
filtered = filtered.filter((triplet) => {
|
||||
const sourceMatches =
|
||||
isEpisodeNode(triplet.sourceNode) &&
|
||||
triplet.sourceNode.attributes?.content?.toLowerCase().includes(query);
|
||||
triplet.sourceNode.attributes?.content
|
||||
?.toLowerCase()
|
||||
.includes(query);
|
||||
const targetMatches =
|
||||
isEpisodeNode(triplet.targetNode) &&
|
||||
triplet.targetNode.attributes?.content?.toLowerCase().includes(query);
|
||||
triplet.targetNode.attributes?.content
|
||||
?.toLowerCase()
|
||||
.includes(query);
|
||||
|
||||
return sourceMatches || targetMatches;
|
||||
});
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { useState, useEffect, type ReactNode } from "react";
|
||||
import { useFetcher } from "@remix-run/react";
|
||||
import { AlertCircle, Loader2 } from "lucide-react";
|
||||
import { AlertCircle, File, Loader2, MessageSquare } from "lucide-react";
|
||||
import { Badge, BadgeColor } from "../ui/badge";
|
||||
import { type LogItem } from "~/hooks/use-logs";
|
||||
import Markdown from "react-markdown";
|
||||
@ -8,6 +8,7 @@ import { getIconForAuthorise } from "../icon-utils";
|
||||
import { cn, formatString } from "~/lib/utils";
|
||||
import { getStatusColor } from "./utils";
|
||||
import { format } from "date-fns";
|
||||
import { SpaceDropdown } from "../spaces/space-dropdown";
|
||||
|
||||
interface LogDetailsProps {
|
||||
log: LogItem;
|
||||
@ -33,13 +34,13 @@ function PropertyItem({
|
||||
if (!value) return null;
|
||||
|
||||
return (
|
||||
<div className="flex items-center py-1">
|
||||
<span className="text-muted-foreground min-w-[160px]">{label}</span>
|
||||
<div className="flex items-center py-1 !text-base">
|
||||
<span className="text-muted-foreground min-w-[120px]">{label}</span>
|
||||
|
||||
{variant === "status" ? (
|
||||
<Badge
|
||||
className={cn(
|
||||
"!bg-grayAlpha-100 text-muted-foreground h-7 rounded px-4 text-xs",
|
||||
"text-foreground h-7 items-center gap-2 rounded !bg-transparent px-4.5 !text-base",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
@ -49,7 +50,13 @@ function PropertyItem({
|
||||
{value}
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge variant={variant} className={cn("h-7 rounded px-4", className)}>
|
||||
<Badge
|
||||
variant={variant}
|
||||
className={cn(
|
||||
"h-7 items-center gap-2 rounded bg-transparent px-4 !text-base",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{icon}
|
||||
{value}
|
||||
</Badge>
|
||||
@ -73,10 +80,10 @@ interface EpisodeFactsResponse {
|
||||
|
||||
function getStatusValue(status: string) {
|
||||
if (status === "PENDING") {
|
||||
return "In Queue";
|
||||
return formatString("IN QUEUE");
|
||||
}
|
||||
|
||||
return status;
|
||||
return formatString(status);
|
||||
}
|
||||
|
||||
export function LogDetails({ log }: LogDetailsProps) {
|
||||
@ -113,6 +120,9 @@ export function LogDetails({ log }: LogDetailsProps) {
|
||||
} else if (log.episodeUUID) {
|
||||
setFactsLoading(true);
|
||||
fetcher.load(`/api/v1/episodes/${log.episodeUUID}/facts`);
|
||||
} else {
|
||||
setFacts([]);
|
||||
setInvalidFacts([]);
|
||||
}
|
||||
}, [log.episodeUUID, log.data?.type, log.data?.episodes, facts.length]);
|
||||
|
||||
@ -129,41 +139,8 @@ export function LogDetails({ log }: LogDetailsProps) {
|
||||
return (
|
||||
<div className="flex h-full w-full flex-col items-center overflow-auto">
|
||||
<div className="max-w-4xl">
|
||||
<div className="px-4 pt-4">
|
||||
<div className="mb-4 flex w-full items-center justify-between">
|
||||
<span>Episode Details</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-10 px-4">
|
||||
<div className="mt-5 mb-5 px-4">
|
||||
<div className="space-y-1">
|
||||
{log.data?.type === "DOCUMENT" && log.data?.episodes ? (
|
||||
<PropertyItem
|
||||
label="Episodes"
|
||||
value={
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{log.data.episodes.map(
|
||||
(episodeId: string, index: number) => (
|
||||
<Badge
|
||||
key={index}
|
||||
variant="outline"
|
||||
className="text-xs"
|
||||
>
|
||||
{episodeId}
|
||||
</Badge>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
variant="secondary"
|
||||
/>
|
||||
) : (
|
||||
<PropertyItem
|
||||
label="Episode Id"
|
||||
value={log.episodeUUID}
|
||||
variant="secondary"
|
||||
/>
|
||||
)}
|
||||
<PropertyItem
|
||||
label="Session Id"
|
||||
value={log.data?.sessionId?.toLowerCase()}
|
||||
@ -174,6 +151,13 @@ export function LogDetails({ log }: LogDetailsProps) {
|
||||
value={formatString(
|
||||
log.data?.type ? log.data.type.toLowerCase() : "conversation",
|
||||
)}
|
||||
icon={
|
||||
log.data?.type === "CONVERSATION" ? (
|
||||
<MessageSquare size={16} />
|
||||
) : (
|
||||
<File size={16} />
|
||||
)
|
||||
}
|
||||
variant="secondary"
|
||||
/>
|
||||
<PropertyItem
|
||||
@ -192,15 +176,28 @@ export function LogDetails({ log }: LogDetailsProps) {
|
||||
variant="status"
|
||||
statusColor={log.status && getStatusColor(log.status)}
|
||||
/>
|
||||
|
||||
{/* Space Assignment for CONVERSATION type */}
|
||||
{log.data.type.toLowerCase() === "conversation" &&
|
||||
log?.episodeUUID && (
|
||||
<div className="mt-2 flex items-start py-1">
|
||||
<span className="text-muted-foreground min-w-[120px]">
|
||||
Spaces
|
||||
</span>
|
||||
|
||||
<SpaceDropdown
|
||||
className="px-3"
|
||||
episodeIds={[log.episodeUUID]}
|
||||
selectedSpaceIds={log.spaceIds || []}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Error Details */}
|
||||
{log.error && (
|
||||
<div className="mb-6 px-4">
|
||||
<div className="mb-2 flex w-full items-center justify-between">
|
||||
<span>Error Details</span>
|
||||
</div>
|
||||
<div className="bg-destructive/10 rounded-md p-3">
|
||||
<div className="flex items-start gap-2 text-red-600">
|
||||
<AlertCircle className="mt-0.5 h-4 w-4 flex-shrink-0" />
|
||||
@ -212,21 +209,63 @@ export function LogDetails({ log }: LogDetailsProps) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col items-center p-4 pt-0">
|
||||
<div className="mb-2 flex w-full items-center justify-between">
|
||||
<span>Content</span>
|
||||
</div>
|
||||
{/* Log Content */}
|
||||
<div className="mb-4 w-full text-sm break-words whitespace-pre-wrap">
|
||||
<div className="rounded-md">
|
||||
<Markdown>{log.ingestText}</Markdown>
|
||||
{log.data?.type === "CONVERSATION" && (
|
||||
<div className="flex flex-col items-center p-4 pt-0">
|
||||
{/* Log Content */}
|
||||
<div className="mb-4 w-full break-words whitespace-pre-wrap">
|
||||
<div className="rounded-md">
|
||||
<Markdown>{log.ingestText}</Markdown>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Episodes List for DOCUMENT type */}
|
||||
{log.data?.type === "DOCUMENT" && log.episodeDetails?.length > 0 && (
|
||||
<div className="mb-6 px-4">
|
||||
<div className="mb-2 flex w-full items-center justify-between font-medium">
|
||||
<span>Episodes ({log.episodeDetails.length})</span>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
{log.episodeDetails.map((episode: any, index: number) => (
|
||||
<div
|
||||
key={episode.uuid}
|
||||
className="bg-grayAlpha-100 flex flex-col gap-3 rounded-md p-3"
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="flex min-w-0 flex-1 flex-col gap-1">
|
||||
<span className="text-muted-foreground text-xs">
|
||||
Episode {index + 1}
|
||||
</span>
|
||||
<span className="truncate font-mono text-xs">
|
||||
{episode.uuid}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex-shrink-0">
|
||||
<SpaceDropdown
|
||||
episodeIds={[episode.uuid]}
|
||||
selectedSpaceIds={episode.spaceIds || []}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/* Episode Content */}
|
||||
<div className="border-grayAlpha-200 border-t pt-3">
|
||||
<div className="text-muted-foreground mb-1 text-xs">
|
||||
Content
|
||||
</div>
|
||||
<div className="text-sm break-words whitespace-pre-wrap">
|
||||
<Markdown>{episode.content}</Markdown>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Episode Facts */}
|
||||
<div className="mb-6 px-4">
|
||||
<div className="mb-2 flex w-full items-center justify-between">
|
||||
<div className="mb-2 flex w-full items-center justify-between font-medium">
|
||||
<span>Facts</span>
|
||||
</div>
|
||||
<div className="rounded-md">
|
||||
|
||||
@ -41,7 +41,6 @@ export const LogOptions = ({ id }: LogOptionsProps) => {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
console.log(deleteFetcher.state, deleteFetcher.data);
|
||||
if (deleteFetcher.state === "idle" && deleteFetcher.data?.success) {
|
||||
navigate(`/home/inbox`);
|
||||
}
|
||||
|
||||
@ -4,6 +4,7 @@ import { type LogItem } from "~/hooks/use-logs";
|
||||
import { getIconForAuthorise } from "../icon-utils";
|
||||
import { useNavigate, useParams } from "@remix-run/react";
|
||||
import { getStatusColor, getStatusValue } from "./utils";
|
||||
import { File, MessageSquare } from "lucide-react";
|
||||
|
||||
interface LogTextCollapseProps {
|
||||
text?: string;
|
||||
@ -49,9 +50,13 @@ export function LogTextCollapse({ text, log }: LogTextCollapseProps) {
|
||||
};
|
||||
|
||||
const getIngestType = (log: LogItem) => {
|
||||
const type = log.type ?? log.data.type ?? "Conversation";
|
||||
const type = log.type ?? log.data.type ?? "CONVERSATION";
|
||||
|
||||
return type[0].toUpperCase();
|
||||
return type === "CONVERSATION" ? (
|
||||
<MessageSquare size={14} />
|
||||
) : (
|
||||
<File size={14} />
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
@ -100,7 +105,7 @@ export function LogTextCollapse({ text, log }: LogTextCollapseProps) {
|
||||
<div className="flex items-center gap-1">
|
||||
<Badge
|
||||
className={cn(
|
||||
"!bg-grayAlpha-100 text-muted-foreground rounded text-xs",
|
||||
"text-muted-foreground rounded !bg-transparent text-xs",
|
||||
)}
|
||||
>
|
||||
{getIngestType(log)}
|
||||
|
||||
@ -22,5 +22,5 @@ export function getStatusValue(status: string) {
|
||||
return formatString("In Queue");
|
||||
}
|
||||
|
||||
return status;
|
||||
return formatString(status);
|
||||
}
|
||||
|
||||
@ -10,6 +10,7 @@ import {
|
||||
import { type LogItem } from "~/hooks/use-logs";
|
||||
import { ScrollManagedList } from "../virtualized-list";
|
||||
import { LogTextCollapse } from "./log-text-collapse";
|
||||
import { LoaderCircle } from "lucide-react";
|
||||
|
||||
interface VirtualLogsListProps {
|
||||
logs: LogItem[];
|
||||
@ -139,7 +140,7 @@ export function VirtualLogsList({
|
||||
|
||||
{isLoading && (
|
||||
<div className="text-muted-foreground p-4 text-center text-sm">
|
||||
Loading more logs...
|
||||
<LoaderCircle size={18} className="mr-1 animate-spin" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -139,6 +139,7 @@ export default function OnboardingQuestionComponent({
|
||||
variant="ghost"
|
||||
size="xl"
|
||||
onClick={onPrevious}
|
||||
disabled={loading}
|
||||
className="rounded-lg px-4 py-2"
|
||||
>
|
||||
Previous
|
||||
@ -151,7 +152,7 @@ export default function OnboardingQuestionComponent({
|
||||
size="xl"
|
||||
onClick={onNext}
|
||||
isLoading={!!loading}
|
||||
disabled={!isValid()}
|
||||
disabled={!isValid() || loading}
|
||||
className="rounded-lg px-4 py-2"
|
||||
>
|
||||
{isLast ? "Complete Profile" : "Continue"}
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import * as React from "react";
|
||||
import { useHotkeys } from "react-hotkeys-hook";
|
||||
|
||||
import {
|
||||
Sidebar,
|
||||
@ -12,14 +13,20 @@ import {
|
||||
Columns3,
|
||||
Inbox,
|
||||
LayoutGrid,
|
||||
LoaderCircle,
|
||||
MessageSquare,
|
||||
Network,
|
||||
Plus,
|
||||
} from "lucide-react";
|
||||
import { NavMain } from "./nav-main";
|
||||
import { useUser } from "~/hooks/useUser";
|
||||
import { NavUser } from "./nav-user";
|
||||
import Logo from "../logo/logo";
|
||||
import { ConversationList } from "../conversation";
|
||||
import { Button } from "../ui";
|
||||
import { Project } from "../icons/project";
|
||||
import { AddMemoryCommand } from "../command-bar/add-memory-command";
|
||||
import { AddMemoryDialog } from "../command-bar/memory-dialog.client";
|
||||
|
||||
const data = {
|
||||
navMain: [
|
||||
@ -41,7 +48,7 @@ const data = {
|
||||
{
|
||||
title: "Spaces",
|
||||
url: "/home/space",
|
||||
icon: Columns3,
|
||||
icon: Project,
|
||||
},
|
||||
{
|
||||
title: "Integrations",
|
||||
@ -54,33 +61,57 @@ const data = {
|
||||
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
||||
const user = useUser();
|
||||
|
||||
return (
|
||||
<Sidebar
|
||||
variant="inset"
|
||||
{...props}
|
||||
className="bg-background h-[100vh] py-2"
|
||||
>
|
||||
<SidebarHeader>
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<div className="mt-1 ml-1 flex w-full items-center justify-start gap-2">
|
||||
<Logo size={20} />
|
||||
C.O.R.E.
|
||||
</div>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarHeader>
|
||||
<SidebarContent>
|
||||
<NavMain items={data.navMain} />
|
||||
<div className="mt-4 flex h-full flex-col">
|
||||
<h2 className="text-muted-foreground px-4 text-sm"> History </h2>
|
||||
<ConversationList />
|
||||
</div>
|
||||
</SidebarContent>
|
||||
const [showAddMemory, setShowAddMemory] = React.useState(false);
|
||||
|
||||
<SidebarFooter className="px-2">
|
||||
<NavUser user={user} />
|
||||
</SidebarFooter>
|
||||
</Sidebar>
|
||||
// Open command bar with Meta+K (Cmd+K on Mac, Ctrl+K on Windows/Linux)
|
||||
useHotkeys("meta+k", (e) => {
|
||||
e.preventDefault();
|
||||
setShowAddMemory(true);
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<Sidebar
|
||||
variant="inset"
|
||||
{...props}
|
||||
className="bg-background h-[100vh] py-2"
|
||||
>
|
||||
<SidebarHeader>
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem className="flex justify-center">
|
||||
<div className="mt-1 ml-1 flex w-full items-center justify-start gap-2">
|
||||
<Logo size={20} />
|
||||
C.O.R.E.
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="secondary"
|
||||
isActive
|
||||
size="sm"
|
||||
className="rounded"
|
||||
onClick={() => setShowAddMemory(true)}
|
||||
>
|
||||
<Plus size={16} />
|
||||
</Button>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarHeader>
|
||||
<SidebarContent>
|
||||
<NavMain items={data.navMain} />
|
||||
<div className="mt-4 flex h-full flex-col">
|
||||
<h2 className="text-muted-foreground px-4 text-sm"> History </h2>
|
||||
<ConversationList />
|
||||
</div>
|
||||
</SidebarContent>
|
||||
|
||||
<SidebarFooter className="flex flex-col px-2">
|
||||
<NavUser user={user} />
|
||||
</SidebarFooter>
|
||||
</Sidebar>
|
||||
|
||||
{showAddMemory && (
|
||||
<AddMemoryDialog open={showAddMemory} onOpenChange={setShowAddMemory} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@ -67,6 +67,15 @@ export function NavUser({ user }: { user: ExtendedUser }) {
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
navigate("/settings/billing");
|
||||
}}
|
||||
>
|
||||
<div>{user.availableCredits} credits</div>
|
||||
</Button>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
);
|
||||
|
||||
@ -17,8 +17,8 @@ interface SpaceCardProps {
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
autoMode: boolean;
|
||||
statementCount: number | null;
|
||||
summary: string | null;
|
||||
contextCount?: number | null;
|
||||
themes?: string[];
|
||||
};
|
||||
}
|
||||
@ -46,13 +46,17 @@ export function SpaceCard({ space }: SpaceCardProps) {
|
||||
</div>
|
||||
<CardTitle className="text-base">{space.name}</CardTitle>
|
||||
<CardDescription className="line-clamp-2 text-xs">
|
||||
{space.description || space.summary || "Knowledge space"}
|
||||
<p
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: space.description || space.summary || "Knowledge space",
|
||||
}}
|
||||
></p>
|
||||
</CardDescription>
|
||||
<div className="text-muted-foreground mt-2 flex items-center justify-between text-xs">
|
||||
{space.statementCount && space.statementCount > 0 && (
|
||||
{space.contextCount && space.contextCount > 0 && (
|
||||
<div>
|
||||
{space.statementCount} fact
|
||||
{space.statementCount !== 1 ? "s" : ""}
|
||||
{space.contextCount} episode
|
||||
{space.contextCount !== 1 ? "s" : ""}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
167
apps/webapp/app/components/spaces/space-dropdown.tsx
Normal file
167
apps/webapp/app/components/spaces/space-dropdown.tsx
Normal file
@ -0,0 +1,167 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { Check, Plus, X } from "lucide-react";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverPortal,
|
||||
PopoverTrigger,
|
||||
} from "~/components/ui/popover";
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from "~/components/ui/command";
|
||||
import { Badge } from "~/components/ui/badge";
|
||||
import { cn } from "~/lib/utils";
|
||||
import { useFetcher } from "@remix-run/react";
|
||||
import { Project } from "../icons/project";
|
||||
|
||||
interface Space {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
interface SpaceDropdownProps {
|
||||
episodeIds: string[];
|
||||
selectedSpaceIds?: string[];
|
||||
onSpaceChange?: (spaceIds: string[]) => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function SpaceDropdown({
|
||||
episodeIds,
|
||||
selectedSpaceIds = [],
|
||||
onSpaceChange,
|
||||
className,
|
||||
}: SpaceDropdownProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [selectedSpaces, setSelectedSpaces] =
|
||||
useState<string[]>(selectedSpaceIds);
|
||||
const [spaces, setSpaces] = useState<Space[]>([]);
|
||||
const spacesFetcher = useFetcher<{ spaces: Space[] }>();
|
||||
const assignFetcher = useFetcher();
|
||||
|
||||
// Fetch all spaces
|
||||
useEffect(() => {
|
||||
spacesFetcher.load("/api/v1/spaces");
|
||||
}, []);
|
||||
|
||||
// Update spaces when data is fetched
|
||||
useEffect(() => {
|
||||
if (spacesFetcher.data?.spaces) {
|
||||
setSpaces(spacesFetcher.data.spaces);
|
||||
}
|
||||
}, [spacesFetcher.data]);
|
||||
|
||||
const handleSpaceToggle = (spaceId: string) => {
|
||||
const newSelectedSpaces = selectedSpaces.includes(spaceId)
|
||||
? selectedSpaces.filter((id) => id !== spaceId)
|
||||
: [...selectedSpaces, spaceId];
|
||||
|
||||
setSelectedSpaces(newSelectedSpaces);
|
||||
if (episodeIds) {
|
||||
assignFetcher.submit(
|
||||
{
|
||||
episodeIds: JSON.stringify(episodeIds),
|
||||
spaceId,
|
||||
action: selectedSpaces.includes(spaceId) ? "remove" : "assign",
|
||||
},
|
||||
{
|
||||
method: "post",
|
||||
action: "/api/v1/episodes/assign-space",
|
||||
encType: "application/json",
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Call the callback if provided
|
||||
if (onSpaceChange) {
|
||||
onSpaceChange(newSelectedSpaces);
|
||||
}
|
||||
};
|
||||
|
||||
const selectedSpaceObjects = spaces.filter((space) =>
|
||||
selectedSpaces.includes(space.id),
|
||||
);
|
||||
|
||||
const getTrigger = () => {
|
||||
if (selectedSpaceObjects?.length === 1) {
|
||||
return (
|
||||
<>
|
||||
<Project size={14} /> {selectedSpaceObjects[0].name}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (selectedSpaceObjects?.length > 1) {
|
||||
return (
|
||||
<>
|
||||
<Project size={14} /> {selectedSpaceObjects.length} Spaces
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{" "}
|
||||
<Project size={14} />
|
||||
Spaces
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={cn("flex flex-wrap items-center gap-2", className)}>
|
||||
{/* + button to add more spaces */}
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
role="combobox"
|
||||
aria-expanded={open}
|
||||
className="h-7 gap-1 rounded"
|
||||
>
|
||||
{getTrigger()}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverPortal>
|
||||
<PopoverContent className="w-[250px] p-0" align="end">
|
||||
<Command>
|
||||
<CommandInput placeholder="Search spaces..." />
|
||||
<CommandList>
|
||||
<CommandEmpty>No spaces found.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{spaces.map((space) => (
|
||||
<CommandItem
|
||||
key={space.id}
|
||||
value={space.name}
|
||||
onSelect={() => handleSpaceToggle(space.id)}
|
||||
>
|
||||
<Check
|
||||
className={cn(
|
||||
"mr-2 h-4 w-4",
|
||||
selectedSpaces.includes(space.id)
|
||||
? "opacity-100"
|
||||
: "opacity-0",
|
||||
)}
|
||||
/>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-sm">{space.name}</span>
|
||||
</div>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</PopoverPortal>
|
||||
</Popover>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -2,12 +2,27 @@ import { Calendar } from "lucide-react";
|
||||
import { Badge } from "~/components/ui/badge";
|
||||
import type { StatementNode } from "@core/types";
|
||||
import { cn } from "~/lib/utils";
|
||||
import { useNavigate } from "@remix-run/react";
|
||||
import Markdown from "react-markdown";
|
||||
|
||||
interface SpaceFactCardProps {
|
||||
fact: StatementNode;
|
||||
export interface Episode {
|
||||
uuid: string;
|
||||
content: string;
|
||||
originalContent: string;
|
||||
source: any;
|
||||
createdAt: Date;
|
||||
validAt: Date;
|
||||
metadata: any;
|
||||
sessionId: any;
|
||||
logId?: any;
|
||||
}
|
||||
|
||||
export function SpaceFactCard({ fact }: SpaceFactCardProps) {
|
||||
interface SpaceFactCardProps {
|
||||
episode: Episode;
|
||||
}
|
||||
|
||||
export function SpaceEpisodeCard({ episode }: SpaceFactCardProps) {
|
||||
const navigate = useNavigate();
|
||||
const formatDate = (date: Date | string) => {
|
||||
const d = new Date(date);
|
||||
return d.toLocaleDateString("en-US", {
|
||||
@ -17,18 +32,20 @@ export function SpaceFactCard({ fact }: SpaceFactCardProps) {
|
||||
});
|
||||
};
|
||||
|
||||
const displayText = fact.fact;
|
||||
const displayText = episode.originalContent;
|
||||
|
||||
const recallCount =
|
||||
(fact.recallCount?.high ?? 0) + (fact.recallCount?.low ?? 0);
|
||||
const onClick = () => {
|
||||
navigate(`/home/inbox/${episode.logId}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex w-full items-center px-5 pr-2">
|
||||
<div className="group 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-3",
|
||||
"group-hover:bg-grayAlpha-100 flex min-w-[0px] shrink grow cursor-pointer items-start gap-2 rounded-md px-3",
|
||||
)}
|
||||
onClick={onClick}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
@ -37,19 +54,13 @@ 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 items-center justify-start">
|
||||
<div className={cn("truncate text-left")}>{displayText}</div>
|
||||
<Markdown>{displayText}</Markdown>
|
||||
</div>
|
||||
<div className="text-muted-foreground flex shrink-0 items-center justify-end gap-2 text-xs">
|
||||
{!!recallCount && <span>Recalled: {recallCount} times</span>}
|
||||
<Badge variant="secondary" className="rounded text-xs">
|
||||
<Calendar className="h-3 w-3" />
|
||||
{formatDate(fact.validAt)}
|
||||
{formatDate(episode.validAt)}
|
||||
</Badge>
|
||||
{fact.invalidAt && (
|
||||
<Badge variant="destructive" className="rounded text-xs">
|
||||
Invalid since {formatDate(fact.invalidAt)}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -9,7 +9,7 @@ import {
|
||||
} from "~/components/ui/popover";
|
||||
import { Badge } from "~/components/ui/badge";
|
||||
|
||||
interface SpaceFactsFiltersProps {
|
||||
interface SpaceEpisodesFiltersProps {
|
||||
selectedValidDate?: string;
|
||||
selectedSpaceFilter?: string;
|
||||
onValidDateChange: (date?: string) => void;
|
||||
@ -22,34 +22,24 @@ const validDateOptions = [
|
||||
{ value: "last_6_months", label: "Last 6 Months" },
|
||||
];
|
||||
|
||||
const spaceFilterOptions = [
|
||||
{ value: "active", label: "Active Facts" },
|
||||
{ value: "archived", label: "Archived Facts" },
|
||||
{ value: "all", label: "All Facts" },
|
||||
];
|
||||
type FilterStep = "main" | "validDate";
|
||||
|
||||
type FilterStep = "main" | "validDate" | "spaceFilter";
|
||||
|
||||
export function SpaceFactsFilters({
|
||||
export function SpaceEpisodesFilters({
|
||||
selectedValidDate,
|
||||
selectedSpaceFilter,
|
||||
onValidDateChange,
|
||||
onSpaceFilterChange,
|
||||
}: SpaceFactsFiltersProps) {
|
||||
}: SpaceEpisodesFiltersProps) {
|
||||
const [popoverOpen, setPopoverOpen] = useState(false);
|
||||
const [step, setStep] = useState<FilterStep>("main");
|
||||
|
||||
const selectedValidDateLabel = validDateOptions.find(
|
||||
(d) => d.value === selectedValidDate,
|
||||
)?.label;
|
||||
const selectedSpaceFilterLabel = spaceFilterOptions.find(
|
||||
(f) => f.value === selectedSpaceFilter,
|
||||
)?.label;
|
||||
|
||||
const hasFilters = selectedValidDate || selectedSpaceFilter;
|
||||
|
||||
return (
|
||||
<div className="mb-2 flex w-full items-center justify-start gap-2 px-5">
|
||||
<>
|
||||
<Popover
|
||||
open={popoverOpen}
|
||||
onOpenChange={(open) => {
|
||||
@ -79,13 +69,6 @@ export function SpaceFactsFilters({
|
||||
>
|
||||
Valid Date
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="justify-start"
|
||||
onClick={() => setStep("spaceFilter")}
|
||||
>
|
||||
Status
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -122,40 +105,6 @@ export function SpaceFactsFilters({
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{step === "spaceFilter" && (
|
||||
<div className="flex flex-col gap-1 p-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="w-full justify-start"
|
||||
onClick={() => {
|
||||
onSpaceFilterChange(undefined);
|
||||
setPopoverOpen(false);
|
||||
setStep("main");
|
||||
}}
|
||||
>
|
||||
All Facts
|
||||
</Button>
|
||||
{spaceFilterOptions.map((option) => (
|
||||
<Button
|
||||
key={option.value}
|
||||
variant="ghost"
|
||||
className="w-full justify-start"
|
||||
onClick={() => {
|
||||
onSpaceFilterChange(
|
||||
option.value === selectedSpaceFilter
|
||||
? undefined
|
||||
: option.value,
|
||||
);
|
||||
setPopoverOpen(false);
|
||||
setStep("main");
|
||||
}}
|
||||
>
|
||||
{option.label}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</PopoverContent>
|
||||
</PopoverPortal>
|
||||
</Popover>
|
||||
@ -172,17 +121,8 @@ export function SpaceFactsFilters({
|
||||
/>
|
||||
</Badge>
|
||||
)}
|
||||
{selectedSpaceFilter && (
|
||||
<Badge variant="secondary" className="h-7 gap-1 rounded px-2">
|
||||
{selectedSpaceFilterLabel}
|
||||
<X
|
||||
className="hover:text-destructive h-3.5 w-3.5 cursor-pointer"
|
||||
onClick={() => onSpaceFilterChange(undefined)}
|
||||
/>
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -9,25 +9,24 @@ import {
|
||||
} from "react-virtualized";
|
||||
import { Database } from "lucide-react";
|
||||
import { Card, CardContent } from "~/components/ui/card";
|
||||
import type { StatementNode } from "@core/types";
|
||||
import { ScrollManagedList } from "../virtualized-list";
|
||||
import { SpaceFactCard } from "./space-fact-card";
|
||||
import { type Episode, SpaceEpisodeCard } from "./space-episode-card";
|
||||
|
||||
interface SpaceFactsListProps {
|
||||
facts: any[];
|
||||
interface SpaceEpisodesListProps {
|
||||
episodes: any[];
|
||||
hasMore: boolean;
|
||||
loadMore: () => void;
|
||||
isLoading: boolean;
|
||||
height?: number;
|
||||
}
|
||||
|
||||
function FactItemRenderer(
|
||||
function EpisodeItemRenderer(
|
||||
props: ListRowProps,
|
||||
facts: StatementNode[],
|
||||
episodes: Episode[],
|
||||
cache: CellMeasurerCache,
|
||||
) {
|
||||
const { index, key, style, parent } = props;
|
||||
const fact = facts[index];
|
||||
const episode = episodes[index];
|
||||
|
||||
return (
|
||||
<CellMeasurer
|
||||
@ -38,23 +37,23 @@ function FactItemRenderer(
|
||||
rowIndex={index}
|
||||
>
|
||||
<div key={key} style={style} className="pb-2">
|
||||
<SpaceFactCard fact={fact} />
|
||||
<SpaceEpisodeCard episode={episode} />
|
||||
</div>
|
||||
</CellMeasurer>
|
||||
);
|
||||
}
|
||||
|
||||
export function SpaceFactsList({
|
||||
facts,
|
||||
export function SpaceEpisodesList({
|
||||
episodes,
|
||||
hasMore,
|
||||
loadMore,
|
||||
isLoading,
|
||||
}: SpaceFactsListProps) {
|
||||
}: SpaceEpisodesListProps) {
|
||||
// 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
|
||||
defaultHeight: 200, // Default row height for episode cards
|
||||
fixedWidth: true, // Rows have fixed width but dynamic height
|
||||
});
|
||||
}
|
||||
@ -62,17 +61,17 @@ export function SpaceFactsList({
|
||||
|
||||
useEffect(() => {
|
||||
cache.clearAll();
|
||||
}, [facts, cache]);
|
||||
}, [episodes, cache]);
|
||||
|
||||
if (facts.length === 0 && !isLoading) {
|
||||
if (episodes.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 facts found</h3>
|
||||
<h3 className="mb-2 text-lg font-semibold">No Episodes found</h3>
|
||||
<p className="text-muted-foreground">
|
||||
This space doesn't contain any facts yet.
|
||||
This space doesn't contain any episodes yet.
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
@ -81,7 +80,7 @@ export function SpaceFactsList({
|
||||
}
|
||||
|
||||
const isRowLoaded = ({ index }: { index: number }) => {
|
||||
return !!facts[index];
|
||||
return !!episodes[index];
|
||||
};
|
||||
|
||||
const loadMoreRows = async () => {
|
||||
@ -92,14 +91,14 @@ export function SpaceFactsList({
|
||||
};
|
||||
|
||||
const rowRenderer = (props: ListRowProps) => {
|
||||
return FactItemRenderer(props, facts, cache);
|
||||
return EpisodeItemRenderer(props, episodes, cache);
|
||||
};
|
||||
|
||||
const rowHeight = ({ index }: Index) => {
|
||||
return cache.getHeight(index, 0);
|
||||
};
|
||||
|
||||
const itemCount = hasMore ? facts.length + 1 : facts.length;
|
||||
const itemCount = hasMore ? episodes.length + 1 : episodes.length;
|
||||
|
||||
return (
|
||||
<div className="h-full grow overflow-hidden rounded-lg">
|
||||
@ -131,7 +130,7 @@ export function SpaceFactsList({
|
||||
|
||||
{isLoading && (
|
||||
<div className="text-muted-foreground p-4 text-center text-sm">
|
||||
Loading more facts...
|
||||
Loading more episodes...
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@ -9,8 +9,8 @@ interface SpacesGridProps {
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
autoMode: boolean;
|
||||
statementCount: number | null;
|
||||
summary: string | null;
|
||||
contextCount?: number | null;
|
||||
themes?: string[];
|
||||
}>;
|
||||
}
|
||||
|
||||
@ -40,7 +40,7 @@ const CommandDialog = ({
|
||||
<Dialog {...props}>
|
||||
<DialogContent className={cn("overflow-hidden p-0 font-sans")}>
|
||||
<Command
|
||||
className="[&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5"
|
||||
className="[&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-10 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-2 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5"
|
||||
{...commandProps}
|
||||
>
|
||||
{children}
|
||||
@ -141,7 +141,7 @@ const CommandItem = React.forwardRef<
|
||||
<CommandPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"command-item aria-selected:bg-accent aria-selected:text-accent-foreground relative flex cursor-default items-center rounded-sm px-2 py-1 outline-none select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
"command-item aria-selected:bg-accent aria-selected:text-accent-foreground relative flex cursor-default items-center rounded px-2 py-1 outline-none select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@ -15,6 +15,8 @@ export interface LogItem {
|
||||
activityId?: string;
|
||||
episodeUUID?: string;
|
||||
data?: any;
|
||||
spaceIds?: string[];
|
||||
episodeDetails?: any;
|
||||
}
|
||||
|
||||
export interface LogsResponse {
|
||||
|
||||
@ -5,7 +5,8 @@ import { useChanged } from "./useChanged";
|
||||
import { useTypedMatchesData } from "./useTypedMatchData";
|
||||
|
||||
export interface ExtendedUser extends User {
|
||||
availableCredits?: number;
|
||||
availableCredits: number;
|
||||
totalCredits: number;
|
||||
}
|
||||
|
||||
export function useIsImpersonating(matches?: UIMatch[]) {
|
||||
@ -23,7 +24,11 @@ export function useOptionalUser(matches?: UIMatch[]): ExtendedUser | undefined {
|
||||
});
|
||||
|
||||
return routeMatch?.user
|
||||
? { ...routeMatch?.user, availableCredits: routeMatch?.availableCredits }
|
||||
? {
|
||||
...routeMatch?.user,
|
||||
availableCredits: routeMatch?.availableCredits,
|
||||
totalCredits: routeMatch?.totalCredits,
|
||||
}
|
||||
: undefined;
|
||||
}
|
||||
|
||||
|
||||
@ -145,10 +145,8 @@ export const getClusteredGraphData = async (userId: string) => {
|
||||
rel.predicate as predicateLabel,
|
||||
e.uuid as episodeUuid,
|
||||
e.content as episodeContent,
|
||||
e.spaceIds as spaceIds,
|
||||
s.uuid as statementUuid,
|
||||
s.spaceIds as spaceIds,
|
||||
s.fact as fact,
|
||||
s.invalidAt as invalidAt,
|
||||
s.validAt as validAt,
|
||||
s.createdAt as createdAt`,
|
||||
{ userId },
|
||||
@ -169,13 +167,8 @@ export const getClusteredGraphData = async (userId: string) => {
|
||||
|
||||
const predicateLabel = record.get("predicateLabel");
|
||||
const episodeUuid = record.get("episodeUuid");
|
||||
const episodeContent = record.get("episodeContent");
|
||||
const statementUuid = record.get("statementUuid");
|
||||
const clusterIds = record.get("spaceIds");
|
||||
const clusterId = clusterIds ? clusterIds[0] : undefined;
|
||||
const fact = record.get("fact");
|
||||
const invalidAt = record.get("invalidAt");
|
||||
const validAt = record.get("validAt");
|
||||
const createdAt = record.get("createdAt");
|
||||
|
||||
// Create unique edge identifier to avoid duplicates
|
||||
|
||||
@ -2,7 +2,6 @@ import type { Prisma, User } from "@core/database";
|
||||
import type { GoogleProfile } from "@coji/remix-auth-google";
|
||||
import { prisma } from "~/db.server";
|
||||
import { env } from "~/env.server";
|
||||
import { ensureBillingInitialized } from "~/services/billing.server";
|
||||
export type { User } from "@core/database";
|
||||
|
||||
type FindOrCreateMagicLink = {
|
||||
@ -167,7 +166,12 @@ export async function findOrCreateGoogleUser({
|
||||
}
|
||||
|
||||
export async function getUserById(id: User["id"]) {
|
||||
const user = await prisma.user.findUnique({ where: { id } });
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
Workspace: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return null;
|
||||
|
||||
@ -25,7 +25,7 @@ import {
|
||||
type ToastMessage,
|
||||
} from "./models/message.server";
|
||||
import { env } from "./env.server";
|
||||
import { getUser, getUserRemainingCount } from "./services/session.server";
|
||||
import { getUser } from "./services/session.server";
|
||||
import { usePostHog } from "./hooks/usePostHog";
|
||||
import {
|
||||
AppContainer,
|
||||
@ -40,6 +40,7 @@ import {
|
||||
useTheme,
|
||||
} from "remix-themes";
|
||||
import clsx from "clsx";
|
||||
import { getUsageSummary } from "./services/billing.server";
|
||||
|
||||
export const links: LinksFunction = () => [{ rel: "stylesheet", href: styles }];
|
||||
|
||||
@ -50,12 +51,13 @@ export const loader = async ({ request }: LoaderFunctionArgs) => {
|
||||
|
||||
const posthogProjectKey = env.POSTHOG_PROJECT_KEY;
|
||||
const user = await getUser(request);
|
||||
const usage = await getUserRemainingCount(request);
|
||||
const usageSummary = await getUsageSummary(user?.Workspace?.id as string);
|
||||
|
||||
return typedjson(
|
||||
{
|
||||
user: user,
|
||||
availableCredits: usage?.availableCredits ?? 0,
|
||||
availableCredits: usageSummary?.credits.available ?? 0,
|
||||
totalCredits: usageSummary?.credits.monthly ?? 0,
|
||||
toastMessage,
|
||||
theme: getTheme(),
|
||||
posthogProjectKey,
|
||||
|
||||
66
apps/webapp/app/routes/api.v1.episodes.assign-space.ts
Normal file
66
apps/webapp/app/routes/api.v1.episodes.assign-space.ts
Normal file
@ -0,0 +1,66 @@
|
||||
import { z } from "zod";
|
||||
import { createHybridActionApiRoute } from "~/services/routeBuilders/apiBuilder.server";
|
||||
import { SpaceService } from "~/services/space.server";
|
||||
import { json } from "@remix-run/node";
|
||||
|
||||
const spaceService = new SpaceService();
|
||||
|
||||
// Schema for assigning episodes to space
|
||||
const AssignEpisodesSchema = z.object({
|
||||
episodeIds: z.string().transform((val) => JSON.parse(val) as string[]),
|
||||
spaceId: z.string(),
|
||||
action: z.enum(["assign", "remove"]),
|
||||
});
|
||||
|
||||
const { action } = createHybridActionApiRoute(
|
||||
{
|
||||
body: AssignEpisodesSchema,
|
||||
allowJWT: true,
|
||||
authorization: {
|
||||
action: "manage",
|
||||
},
|
||||
corsStrategy: "all",
|
||||
},
|
||||
async ({ authentication, body }) => {
|
||||
const userId = authentication.userId;
|
||||
const { episodeIds, spaceId, action: actionType } = body;
|
||||
|
||||
try {
|
||||
if (actionType === "assign") {
|
||||
await spaceService.assignEpisodesToSpace(episodeIds, spaceId, userId);
|
||||
return json({
|
||||
success: true,
|
||||
message: `Successfully assigned ${episodeIds.length} episode(s) to space`,
|
||||
});
|
||||
} else if (actionType === "remove") {
|
||||
await spaceService.removeEpisodesFromSpace(episodeIds, spaceId, userId);
|
||||
return json({
|
||||
success: true,
|
||||
message: `Successfully removed ${episodeIds.length} episode(s) from space`,
|
||||
});
|
||||
}
|
||||
|
||||
return json(
|
||||
{
|
||||
error: "Invalid action type",
|
||||
success: false,
|
||||
},
|
||||
{ status: 400 },
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Error managing episode space assignment:", error);
|
||||
return json(
|
||||
{
|
||||
error:
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "Failed to manage episode space assignment",
|
||||
success: false,
|
||||
},
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
export { action };
|
||||
@ -24,8 +24,11 @@ const loader = createHybridLoaderApiRoute(
|
||||
corsStrategy: "all",
|
||||
allowJWT: true,
|
||||
},
|
||||
async ({ params }) => {
|
||||
const formattedLog = await getIngestionQueueForFrontend(params.logId);
|
||||
async ({ params, authentication }) => {
|
||||
const formattedLog = await getIngestionQueueForFrontend(
|
||||
params.logId,
|
||||
authentication.userId,
|
||||
);
|
||||
|
||||
return json({ log: formattedLog });
|
||||
},
|
||||
|
||||
@ -1,49 +0,0 @@
|
||||
import { z } from "zod";
|
||||
import { createActionApiRoute } from "~/services/routeBuilders/apiBuilder.server";
|
||||
import { SpaceService } from "~/services/space.server";
|
||||
import { json } from "@remix-run/node";
|
||||
|
||||
const spaceService = new SpaceService();
|
||||
|
||||
// Schema for space ID parameter
|
||||
const SpaceParamsSchema = z.object({
|
||||
spaceId: z.string(),
|
||||
});
|
||||
|
||||
const { loader } = createActionApiRoute(
|
||||
{
|
||||
params: SpaceParamsSchema,
|
||||
allowJWT: true,
|
||||
authorization: {
|
||||
action: "search",
|
||||
},
|
||||
corsStrategy: "all",
|
||||
},
|
||||
async ({ authentication, params }) => {
|
||||
const userId = authentication.userId;
|
||||
const { spaceId } = params;
|
||||
|
||||
// Verify space exists and belongs to user
|
||||
const space = await spaceService.getSpace(spaceId, userId);
|
||||
if (!space) {
|
||||
return json({ error: "Space not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
// Get statements in the space
|
||||
const statements = await spaceService.getSpaceStatements(spaceId, userId);
|
||||
|
||||
return json({
|
||||
deprecated: true,
|
||||
deprecationMessage: "This endpoint is deprecated. Use /api/v1/spaces/{spaceId}/episodes instead. Spaces now work with episodes directly.",
|
||||
newEndpoint: `/api/v1/spaces/${spaceId}/episodes`,
|
||||
statements,
|
||||
space: {
|
||||
uuid: space.uuid,
|
||||
name: space.name,
|
||||
statementCount: statements.length
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
export { loader };
|
||||
@ -16,19 +16,6 @@ const CreateSpaceSchema = z.object({
|
||||
description: z.string().optional(),
|
||||
});
|
||||
|
||||
// Schema for bulk operations
|
||||
const BulkOperationSchema = z.object({
|
||||
intent: z.enum([
|
||||
"assign_statements",
|
||||
"remove_statements",
|
||||
"bulk_assign",
|
||||
"initialize_space_ids",
|
||||
]),
|
||||
spaceId: z.string().optional(),
|
||||
statementIds: z.array(z.string()).optional(),
|
||||
spaceIds: z.array(z.string()).optional(),
|
||||
});
|
||||
|
||||
// Search query schema
|
||||
const SearchParamsSchema = z.object({
|
||||
q: z.string().optional(),
|
||||
@ -36,7 +23,7 @@ const SearchParamsSchema = z.object({
|
||||
|
||||
const { action } = createHybridActionApiRoute(
|
||||
{
|
||||
body: z.union([CreateSpaceSchema, BulkOperationSchema]),
|
||||
body: CreateSpaceSchema,
|
||||
allowJWT: true,
|
||||
authorization: {
|
||||
action: "manage",
|
||||
@ -82,96 +69,6 @@ const { action } = createHybridActionApiRoute(
|
||||
return json({ space, success: true });
|
||||
}
|
||||
|
||||
if (request.method === "PUT") {
|
||||
// Bulk operations
|
||||
if (!body || !("intent" in body)) {
|
||||
return json({ error: "Intent is required" }, { status: 400 });
|
||||
}
|
||||
|
||||
switch (body.intent) {
|
||||
case "assign_statements": {
|
||||
if (!body.spaceId || !body.statementIds) {
|
||||
return json(
|
||||
{ error: "Space ID and statement IDs are required" },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
const result = await spaceService.assignStatementsToSpace(
|
||||
body.statementIds,
|
||||
body.spaceId,
|
||||
authentication.userId,
|
||||
);
|
||||
|
||||
if (result.success) {
|
||||
return json({
|
||||
success: true,
|
||||
message: `Assigned ${result.statementsUpdated} statements to space`,
|
||||
statementsUpdated: result.statementsUpdated,
|
||||
});
|
||||
} else {
|
||||
return json({ error: result.error }, { status: 400 });
|
||||
}
|
||||
}
|
||||
|
||||
case "remove_statements": {
|
||||
if (!body.spaceId || !body.statementIds) {
|
||||
return json(
|
||||
{ error: "Space ID and statement IDs are required" },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
const result = await spaceService.removeStatementsFromSpace(
|
||||
body.statementIds,
|
||||
body.spaceId,
|
||||
authentication.userId,
|
||||
);
|
||||
|
||||
if (result.success) {
|
||||
return json({
|
||||
success: true,
|
||||
message: `Removed ${result.statementsUpdated} statements from space`,
|
||||
statementsUpdated: result.statementsUpdated,
|
||||
});
|
||||
} else {
|
||||
return json({ error: result.error }, { status: 400 });
|
||||
}
|
||||
}
|
||||
|
||||
case "bulk_assign": {
|
||||
if (!body.statementIds || !body.spaceIds) {
|
||||
return json(
|
||||
{ error: "Statement IDs and space IDs are required" },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
const results = await spaceService.bulkAssignStatements(
|
||||
body.statementIds,
|
||||
body.spaceIds,
|
||||
authentication.userId,
|
||||
);
|
||||
|
||||
return json({ results, success: true });
|
||||
}
|
||||
|
||||
case "initialize_space_ids": {
|
||||
const updatedCount = await spaceService.initializeSpaceIds(
|
||||
authentication.userId,
|
||||
);
|
||||
return json({
|
||||
success: true,
|
||||
message: `Initialized spaceIds for ${updatedCount} statements`,
|
||||
updatedCount,
|
||||
});
|
||||
}
|
||||
|
||||
default:
|
||||
return json({ error: "Invalid intent" }, { status: 400 });
|
||||
}
|
||||
}
|
||||
|
||||
return json({ error: "Method not allowed" }, { status: 405 });
|
||||
},
|
||||
);
|
||||
|
||||
@ -9,11 +9,11 @@ import { getIngestionQueueForFrontend } from "~/services/ingestionLogs.server";
|
||||
import { requireUserId } from "~/services/session.server";
|
||||
|
||||
export async function loader({ request, params }: LoaderFunctionArgs) {
|
||||
await requireUserId(request);
|
||||
const userId = await requireUserId(request);
|
||||
const logId = params.logId;
|
||||
|
||||
try {
|
||||
const log = await getIngestionQueueForFrontend(logId as string);
|
||||
const log = await getIngestionQueueForFrontend(logId as string, userId);
|
||||
return json({ log: log });
|
||||
} catch (e) {
|
||||
return json({ log: null });
|
||||
|
||||
@ -3,11 +3,13 @@ import { type LoaderFunctionArgs } from "@remix-run/server-runtime";
|
||||
import { useLoaderData } from "@remix-run/react";
|
||||
import { requireUserId } from "~/services/session.server";
|
||||
import { SpaceService } from "~/services/space.server";
|
||||
import { SpaceFactsFilters } from "~/components/spaces/space-facts-filters";
|
||||
import { SpaceFactsList } from "~/components/spaces/space-facts-list";
|
||||
import { SpaceEpisodesFilters } from "~/components/spaces/space-episode-filters";
|
||||
import { SpaceEpisodesList } from "~/components/spaces/space-episodes-list";
|
||||
|
||||
import { ClientOnly } from "remix-utils/client-only";
|
||||
import { LoaderCircle } from "lucide-react";
|
||||
import { getLogByEpisode } from "~/services/ingestionLogs.server";
|
||||
import { Button } from "~/components/ui";
|
||||
|
||||
export async function loader({ request, params }: LoaderFunctionArgs) {
|
||||
const userId = await requireUserId(request);
|
||||
@ -15,16 +17,27 @@ export async function loader({ request, params }: LoaderFunctionArgs) {
|
||||
|
||||
const spaceId = params.spaceId as string;
|
||||
const space = await spaceService.getSpace(spaceId, userId);
|
||||
const statements = await spaceService.getSpaceStatements(spaceId, userId);
|
||||
const episodes = await spaceService.getSpaceEpisodes(spaceId, userId);
|
||||
|
||||
const episodesWithLogData = await Promise.all(
|
||||
episodes.map(async (ep) => {
|
||||
const log = await getLogByEpisode(ep.uuid);
|
||||
|
||||
return {
|
||||
...ep,
|
||||
logId: log?.id,
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
return {
|
||||
space,
|
||||
statements: statements || [],
|
||||
episodes: episodesWithLogData || [],
|
||||
};
|
||||
}
|
||||
|
||||
export default function Facts() {
|
||||
const { statements } = useLoaderData<typeof loader>();
|
||||
export default function Episodes() {
|
||||
const { episodes } = useLoaderData<typeof loader>();
|
||||
const [selectedValidDate, setSelectedValidDate] = useState<
|
||||
string | undefined
|
||||
>();
|
||||
@ -32,42 +45,27 @@ export default function Facts() {
|
||||
string | undefined
|
||||
>();
|
||||
|
||||
// Filter statements based on selected filters
|
||||
const filteredStatements = statements.filter((statement) => {
|
||||
// Filter episodes based on selected filters
|
||||
const filteredEpisodes = episodes.filter((episode) => {
|
||||
// Date filter
|
||||
if (selectedValidDate) {
|
||||
const now = new Date();
|
||||
const statementDate = new Date(statement.validAt);
|
||||
const episodeDate = new Date(episode.createdAt);
|
||||
|
||||
switch (selectedValidDate) {
|
||||
case "last_week":
|
||||
const weekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
|
||||
if (statementDate < weekAgo) return false;
|
||||
if (episodeDate < weekAgo) return false;
|
||||
break;
|
||||
case "last_month":
|
||||
const monthAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
|
||||
if (statementDate < monthAgo) return false;
|
||||
if (episodeDate < monthAgo) return false;
|
||||
break;
|
||||
case "last_6_months":
|
||||
const sixMonthsAgo = new Date(
|
||||
now.getTime() - 180 * 24 * 60 * 60 * 1000,
|
||||
);
|
||||
if (statementDate < sixMonthsAgo) return false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Status filter
|
||||
if (selectedSpaceFilter) {
|
||||
switch (selectedSpaceFilter) {
|
||||
case "active":
|
||||
if (statement.invalidAt) return false;
|
||||
break;
|
||||
case "archived":
|
||||
if (!statement.invalidAt) return false;
|
||||
break;
|
||||
case "all":
|
||||
default:
|
||||
if (episodeDate < sixMonthsAgo) return false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -81,20 +79,22 @@ export default function Facts() {
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full flex-col pt-5">
|
||||
<SpaceFactsFilters
|
||||
selectedValidDate={selectedValidDate}
|
||||
selectedSpaceFilter={selectedSpaceFilter}
|
||||
onValidDateChange={setSelectedValidDate}
|
||||
onSpaceFilterChange={setSelectedSpaceFilter}
|
||||
/>
|
||||
<div className="mb-2 flex w-full items-center justify-start gap-2 px-5">
|
||||
<SpaceEpisodesFilters
|
||||
selectedValidDate={selectedValidDate}
|
||||
selectedSpaceFilter={selectedSpaceFilter}
|
||||
onValidDateChange={setSelectedValidDate}
|
||||
onSpaceFilterChange={setSelectedSpaceFilter}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex h-[calc(100vh_-_56px)] w-full">
|
||||
<ClientOnly
|
||||
fallback={<LoaderCircle className="mr-2 h-4 w-4 animate-spin" />}
|
||||
>
|
||||
{() => (
|
||||
<SpaceFactsList
|
||||
facts={filteredStatements}
|
||||
<SpaceEpisodesList
|
||||
episodes={filteredEpisodes}
|
||||
hasMore={false} // TODO: Implement real pagination
|
||||
loadMore={loadMore}
|
||||
isLoading={false}
|
||||
@ -148,7 +148,7 @@ export default function Overview() {
|
||||
variant="ghost"
|
||||
className="text-muted-foreground mb-1 -ml-2 gap-1"
|
||||
>
|
||||
Summary
|
||||
Context
|
||||
<ChevronDown
|
||||
size={14}
|
||||
className={`transition-transform duration-300 ${
|
||||
|
||||
@ -7,6 +7,9 @@ import { useTypedLoaderData } from "remix-typedjson";
|
||||
import { Outlet, useLocation, useNavigate } from "@remix-run/react";
|
||||
import { SpaceOptions } from "~/components/spaces/space-options";
|
||||
import { LoaderCircle } from "lucide-react";
|
||||
import { Button } from "~/components/ui";
|
||||
import React from "react";
|
||||
import { AddMemoryDialog } from "~/components/command-bar/memory-dialog.client";
|
||||
|
||||
export async function loader({ request, params }: LoaderFunctionArgs) {
|
||||
const userId = await requireUserId(request);
|
||||
@ -23,6 +26,7 @@ export default function Space() {
|
||||
const space = useTypedLoaderData<typeof loader>();
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const [showAddMemory, setShowAddMemory] = React.useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -46,16 +50,10 @@ export default function Space() {
|
||||
onClick: () => navigate(`/home/space/${space.id}/overview`),
|
||||
},
|
||||
{
|
||||
label: "Facts",
|
||||
value: "facts",
|
||||
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`),
|
||||
label: "Episodes",
|
||||
value: "edpisodes",
|
||||
isActive: location.pathname.includes("/episodes"),
|
||||
onClick: () => navigate(`/home/space/${space.id}/episodes`),
|
||||
},
|
||||
]}
|
||||
actionsNode={
|
||||
@ -67,17 +65,33 @@ export default function Space() {
|
||||
}
|
||||
>
|
||||
{() => (
|
||||
<SpaceOptions
|
||||
id={space.id as string}
|
||||
name={space.name}
|
||||
description={space.description}
|
||||
/>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => setShowAddMemory(true)}
|
||||
>
|
||||
Add episode
|
||||
</Button>
|
||||
<SpaceOptions
|
||||
id={space.id as string}
|
||||
name={space.name}
|
||||
description={space.description}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</ClientOnly>
|
||||
}
|
||||
/>
|
||||
<div className="relative flex h-[calc(100vh_-_56px)] w-full flex-col items-center justify-start overflow-auto">
|
||||
<Outlet />
|
||||
|
||||
{showAddMemory && (
|
||||
<AddMemoryDialog
|
||||
open={showAddMemory}
|
||||
onOpenChange={setShowAddMemory}
|
||||
defaultSpaceId={space.id}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@ -153,6 +153,7 @@ export default function Onboarding() {
|
||||
setCurrentQuestion(currentQuestion + 1);
|
||||
} else {
|
||||
setLoading(true);
|
||||
|
||||
// Submit all answers
|
||||
submitAnswers();
|
||||
}
|
||||
|
||||
@ -262,6 +262,7 @@ export default function BillingSettings() {
|
||||
<Progress
|
||||
segments={[{ value: 100 - usageSummary.credits.percentageUsed }]}
|
||||
className="mb-2"
|
||||
color="#c15e50"
|
||||
/>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{usageSummary.credits.percentageUsed}% used this period
|
||||
@ -452,7 +453,7 @@ export default function BillingSettings() {
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="grid gap-6 p-6 md:grid-cols-3">
|
||||
<div className="grid gap-6 md:grid-cols-3">
|
||||
{/* Free Plan */}
|
||||
<Card className="p-6">
|
||||
<div className="mb-4">
|
||||
@ -467,10 +468,10 @@ export default function BillingSettings() {
|
||||
</div>
|
||||
<ul className="mb-6 space-y-2 text-sm">
|
||||
<li className="flex items-start gap-2">
|
||||
<span>Memory facts: 3k/mo</span>
|
||||
<span>Credits: 3k/mo</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<span>NO USAGE BASED</span>
|
||||
<span>No usage based</span>
|
||||
</li>
|
||||
</ul>
|
||||
<Button
|
||||
@ -504,14 +505,15 @@ export default function BillingSettings() {
|
||||
</div>
|
||||
<ul className="mb-6 space-y-2 text-sm">
|
||||
<li className="flex items-start gap-2">
|
||||
<span>Memory facts: 15k/mo</span>
|
||||
<span>Credits: 15k/mo</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<span>$0.299 /1K ADDITIONAL FACTS</span>
|
||||
<span>$0.299 /1K Additional Credits</span>
|
||||
</li>
|
||||
</ul>
|
||||
<Button
|
||||
className="w-full"
|
||||
variant="secondary"
|
||||
disabled={
|
||||
usageSummary.plan.type === "PRO" ||
|
||||
fetcher.state === "submitting"
|
||||
@ -540,14 +542,15 @@ export default function BillingSettings() {
|
||||
</div>
|
||||
<ul className="mb-6 space-y-2 text-sm">
|
||||
<li className="flex items-start gap-2">
|
||||
<span>Memory facts: 100k/mo</span>
|
||||
<span>Credits: 100k/mo</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<span>$0.249 /1K ADDITIONAL FACTS</span>
|
||||
<span>$0.249 /1K Additional Credits</span>
|
||||
</li>
|
||||
</ul>
|
||||
<Button
|
||||
className="w-full"
|
||||
variant="secondary"
|
||||
disabled={
|
||||
usageSummary.plan.type === "MAX" ||
|
||||
fetcher.state === "submitting"
|
||||
|
||||
@ -160,6 +160,10 @@ export async function ensureBillingInitialized(workspaceId: string) {
|
||||
* Get workspace usage summary
|
||||
*/
|
||||
export async function getUsageSummary(workspaceId: string) {
|
||||
if (!workspaceId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Ensure billing records exist for existing accounts
|
||||
await ensureBillingInitialized(workspaceId);
|
||||
|
||||
|
||||
@ -1,5 +1,9 @@
|
||||
import { runQuery } from "~/lib/neo4j.server";
|
||||
import { type StatementNode, type EntityNode, type EpisodicNode } from "@core/types";
|
||||
import {
|
||||
type StatementNode,
|
||||
type EntityNode,
|
||||
type EpisodicNode,
|
||||
} from "@core/types";
|
||||
|
||||
export async function saveEpisode(episode: EpisodicNode): Promise<string> {
|
||||
const query = `
|
||||
@ -72,6 +76,8 @@ export async function getEpisode(uuid: string): Promise<EpisodicNode | null> {
|
||||
userId: episode.userId,
|
||||
space: episode.space,
|
||||
sessionId: episode.sessionId,
|
||||
recallCount: episode.recallCount,
|
||||
spaceIds: episode.spaceIds,
|
||||
};
|
||||
}
|
||||
|
||||
@ -140,7 +146,7 @@ export async function searchEpisodesByEmbedding(params: {
|
||||
}) {
|
||||
const limit = params.limit || 100;
|
||||
const query = `
|
||||
CALL db.index.vector.queryNodes('episode_embedding', ${limit*2}, $embedding)
|
||||
CALL db.index.vector.queryNodes('episode_embedding', ${limit * 2}, $embedding)
|
||||
YIELD node AS episode
|
||||
WHERE episode.userId = $userId
|
||||
WITH episode, gds.similarity.cosine(episode.contentEmbedding, $embedding) AS score
|
||||
@ -285,7 +291,7 @@ export async function getRelatedEpisodesEntities(params: {
|
||||
}) {
|
||||
const limit = params.limit || 100;
|
||||
const query = `
|
||||
CALL db.index.vector.queryNodes('episode_embedding', ${limit*2}, $embedding)
|
||||
CALL db.index.vector.queryNodes('episode_embedding', ${limit * 2}, $embedding)
|
||||
YIELD node AS episode
|
||||
WHERE episode.userId = $userId
|
||||
WITH episode, gds.similarity.cosine(episode.contentEmbedding, $embedding) AS score
|
||||
|
||||
@ -157,8 +157,12 @@ export async function deleteSpace(
|
||||
RETURN count(s) as updatedStatements
|
||||
`;
|
||||
|
||||
const cleanupStatementsResult = await runQuery(cleanupStatementsQuery, { userId, spaceId });
|
||||
const updatedStatements = cleanupStatementsResult[0]?.get("updatedStatements") || 0;
|
||||
const cleanupStatementsResult = await runQuery(cleanupStatementsQuery, {
|
||||
userId,
|
||||
spaceId,
|
||||
});
|
||||
const updatedStatements =
|
||||
cleanupStatementsResult[0]?.get("updatedStatements") || 0;
|
||||
|
||||
// 3. Clean up episode references (remove spaceId from spaceIds arrays)
|
||||
const cleanupEpisodesQuery = `
|
||||
@ -168,8 +172,12 @@ export async function deleteSpace(
|
||||
RETURN count(e) as updatedEpisodes
|
||||
`;
|
||||
|
||||
const cleanupEpisodesResult = await runQuery(cleanupEpisodesQuery, { userId, spaceId });
|
||||
const updatedEpisodes = cleanupEpisodesResult[0]?.get("updatedEpisodes") || 0;
|
||||
const cleanupEpisodesResult = await runQuery(cleanupEpisodesQuery, {
|
||||
userId,
|
||||
spaceId,
|
||||
});
|
||||
const updatedEpisodes =
|
||||
cleanupEpisodesResult[0]?.get("updatedEpisodes") || 0;
|
||||
|
||||
// 4. Delete the space node and all its relationships
|
||||
const deleteQuery = `
|
||||
@ -199,166 +207,6 @@ export async function deleteSpace(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Assign statements to a space
|
||||
*/
|
||||
export async function assignStatementsToSpace(
|
||||
statementIds: string[],
|
||||
spaceId: string,
|
||||
userId: string,
|
||||
): Promise<SpaceAssignmentResult> {
|
||||
try {
|
||||
// Verify space exists and belongs to user
|
||||
const space = await getSpace(spaceId, userId);
|
||||
if (!space) {
|
||||
return {
|
||||
success: false,
|
||||
statementsUpdated: 0,
|
||||
error: "Space not found or access denied",
|
||||
};
|
||||
}
|
||||
|
||||
const query = `
|
||||
MATCH (s:Statement {userId: $userId})
|
||||
WHERE s.uuid IN $statementIds
|
||||
SET s.spaceIds = CASE
|
||||
WHEN s.spaceIds IS NULL THEN [$spaceId]
|
||||
WHEN $spaceId IN s.spaceIds THEN s.spaceIds
|
||||
ELSE s.spaceIds + [$spaceId]
|
||||
END,
|
||||
s.lastSpaceAssignment = datetime(),
|
||||
s.spaceAssignmentMethod = CASE
|
||||
WHEN s.spaceAssignmentMethod IS NULL THEN 'manual'
|
||||
ELSE s.spaceAssignmentMethod
|
||||
END
|
||||
RETURN count(s) as updated
|
||||
`;
|
||||
|
||||
const result = await runQuery(query, { statementIds, spaceId, userId });
|
||||
const updatedCount = result[0]?.get("updated") || 0;
|
||||
|
||||
return {
|
||||
success: true,
|
||||
statementsUpdated: Number(updatedCount),
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
statementsUpdated: 0,
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove statements from a space
|
||||
*/
|
||||
export async function removeStatementsFromSpace(
|
||||
statementIds: string[],
|
||||
spaceId: string,
|
||||
userId: string,
|
||||
): Promise<SpaceAssignmentResult> {
|
||||
try {
|
||||
const query = `
|
||||
MATCH (s:Statement {userId: $userId})
|
||||
WHERE s.uuid IN $statementIds AND s.spaceIds IS NOT NULL AND $spaceId IN s.spaceIds
|
||||
SET s.spaceIds = [id IN s.spaceIds WHERE id <> $spaceId]
|
||||
RETURN count(s) as updated
|
||||
`;
|
||||
|
||||
const result = await runQuery(query, { statementIds, spaceId, userId });
|
||||
const updatedCount = result[0]?.get("updated") || 0;
|
||||
|
||||
return {
|
||||
success: true,
|
||||
statementsUpdated: Number(updatedCount),
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
statementsUpdated: 0,
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all statements in a space
|
||||
*/
|
||||
export async function getSpaceStatements(spaceId: string, userId: string) {
|
||||
const query = `
|
||||
MATCH (s:Statement {userId: $userId})
|
||||
WHERE s.spaceIds IS NOT NULL AND $spaceId IN s.spaceIds AND s.invalidAt IS NULL
|
||||
MATCH (s)-[:HAS_SUBJECT]->(subj:Entity)
|
||||
MATCH (s)-[:HAS_PREDICATE]->(pred:Entity)
|
||||
MATCH (s)-[:HAS_OBJECT]->(obj:Entity)
|
||||
RETURN s, subj.name as subject, pred.name as predicate, obj.name as object
|
||||
ORDER BY s.createdAt DESC
|
||||
`;
|
||||
|
||||
const result = await runQuery(query, { spaceId, userId });
|
||||
|
||||
return result.map((record) => {
|
||||
const statement = record.get("s").properties;
|
||||
return {
|
||||
uuid: statement.uuid,
|
||||
fact: statement.fact,
|
||||
subject: record.get("subject"),
|
||||
predicate: record.get("predicate"),
|
||||
object: record.get("object"),
|
||||
createdAt: new Date(statement.createdAt),
|
||||
validAt: new Date(statement.validAt),
|
||||
invalidAt: statement.invalidAt
|
||||
? new Date(statement.invalidAt)
|
||||
: undefined,
|
||||
spaceIds: statement.spaceIds || [],
|
||||
recallCount: statement.recallCount,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get real-time statement count for a space from Neo4j
|
||||
*/
|
||||
export async function getSpaceStatementCount(
|
||||
spaceId: string,
|
||||
userId: string,
|
||||
): Promise<number> {
|
||||
const query = `
|
||||
MATCH (s:Statement {userId: $userId})
|
||||
WHERE s.spaceIds IS NOT NULL
|
||||
AND $spaceId IN s.spaceIds
|
||||
RETURN count(s) as statementCount
|
||||
`;
|
||||
|
||||
const result = await runQuery(query, { spaceId, userId });
|
||||
return Number(result[0]?.get("statementCount") || 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize spaceIds array for existing statements (migration helper)
|
||||
*/
|
||||
export async function initializeStatementSpaceIds(
|
||||
userId?: string,
|
||||
): Promise<number> {
|
||||
const query = userId
|
||||
? `
|
||||
MATCH (s:Statement {userId: $userId})
|
||||
WHERE s.spaceIds IS NULL
|
||||
SET s.spaceIds = []
|
||||
RETURN count(s) as updated
|
||||
`
|
||||
: `
|
||||
MATCH (s:Statement)
|
||||
WHERE s.spaceIds IS NULL
|
||||
SET s.spaceIds = []
|
||||
RETURN count(s) as updated
|
||||
`;
|
||||
|
||||
const result = await runQuery(query, userId ? { userId } : {});
|
||||
return Number(result[0]?.get("updated") || 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assign episodes to a space using intent-based matching
|
||||
*/
|
||||
@ -508,3 +356,36 @@ export async function getSpaceEpisodeCount(
|
||||
const result = await runQuery(query, { spaceId, userId });
|
||||
return Number(result[0]?.get("episodeCount") || 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get spaces for specific episodes
|
||||
*/
|
||||
export async function getSpacesForEpisodes(
|
||||
episodeIds: string[],
|
||||
userId: string,
|
||||
): Promise<Record<string, string[]>> {
|
||||
const query = `
|
||||
UNWIND $episodeIds as episodeId
|
||||
MATCH (e:Episode {uuid: episodeId, userId: $userId})
|
||||
WHERE e.spaceIds IS NOT NULL AND size(e.spaceIds) > 0
|
||||
RETURN episodeId, e.spaceIds as spaceIds
|
||||
`;
|
||||
|
||||
const result = await runQuery(query, { episodeIds, userId });
|
||||
|
||||
const spacesMap: Record<string, string[]> = {};
|
||||
|
||||
// Initialize all episodes with empty arrays
|
||||
episodeIds.forEach((id) => {
|
||||
spacesMap[id] = [];
|
||||
});
|
||||
|
||||
// Fill in the spaceIds for episodes that have them
|
||||
result.forEach((record) => {
|
||||
const episodeId = record.get("episodeId");
|
||||
const spaceIds = record.get("spaceIds");
|
||||
spacesMap[episodeId] = spaceIds || [];
|
||||
});
|
||||
|
||||
return spacesMap;
|
||||
}
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
import { prisma } from "~/db.server";
|
||||
import { getEpisode } from "./graphModels/episode";
|
||||
import { getSpacesForEpisodes } from "./graphModels/space";
|
||||
|
||||
export async function getIngestionLogs(
|
||||
userId: string,
|
||||
@ -53,7 +55,10 @@ export const getIngestionQueue = async (id: string) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const getIngestionQueueForFrontend = async (id: string) => {
|
||||
export const getIngestionQueueForFrontend = async (
|
||||
id: string,
|
||||
userId: string,
|
||||
) => {
|
||||
// Fetch the specific log by logId
|
||||
const log = await prisma.ingestionQueue.findUnique({
|
||||
where: { id: id },
|
||||
@ -66,6 +71,7 @@ export const getIngestionQueueForFrontend = async (id: string) => {
|
||||
type: true,
|
||||
output: true,
|
||||
data: true,
|
||||
workspaceId: true,
|
||||
activity: {
|
||||
select: {
|
||||
text: true,
|
||||
@ -94,7 +100,7 @@ export const getIngestionQueueForFrontend = async (id: string) => {
|
||||
log.activity?.integrationAccount?.integrationDefinition;
|
||||
const logData = log.data as any;
|
||||
|
||||
const formattedLog = {
|
||||
const formattedLog: any = {
|
||||
id: log.id,
|
||||
source: integrationDef?.name || logData?.source || "Unknown",
|
||||
ingestText:
|
||||
@ -112,9 +118,76 @@ export const getIngestionQueueForFrontend = async (id: string) => {
|
||||
data: log.data,
|
||||
};
|
||||
|
||||
// Fetch space data based on log type
|
||||
if (logData?.type === "CONVERSATION" && formattedLog?.episodeUUID) {
|
||||
// For CONVERSATION type: get spaceIds for the single episode
|
||||
const spacesMap = await getSpacesForEpisodes(
|
||||
[formattedLog.episodeUUID],
|
||||
userId,
|
||||
);
|
||||
formattedLog.spaceIds = spacesMap[formattedLog.episodeUUID] || [];
|
||||
} else if (
|
||||
logData?.type === "DOCUMENT" &&
|
||||
(log.output as any)?.episodes?.length > 0
|
||||
) {
|
||||
// For DOCUMENT type: get episode details and space information for all episodes
|
||||
const episodeIds = (log.output as any)?.episodes;
|
||||
|
||||
// Fetch all episode details in parallel
|
||||
const episodeDetailsPromises = episodeIds.map((episodeId: string) =>
|
||||
getEpisode(episodeId).catch(() => null),
|
||||
);
|
||||
const episodeDetails = await Promise.all(episodeDetailsPromises);
|
||||
|
||||
// Get spaceIds for all episodes
|
||||
const spacesMap = await getSpacesForEpisodes(episodeIds, userId);
|
||||
|
||||
// Combine episode details with space information
|
||||
formattedLog.episodeDetails = episodeIds.map(
|
||||
(episodeId: string, index: number) => {
|
||||
const episode = episodeDetails[index];
|
||||
return {
|
||||
uuid: episodeId,
|
||||
content: episode?.content || episode?.originalContent || "No content",
|
||||
spaceIds: spacesMap[episodeId] || [],
|
||||
};
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return formattedLog;
|
||||
};
|
||||
|
||||
export const getLogByEpisode = async (episodeUuid: string) => {
|
||||
// Find logs where the episode UUID matches either:
|
||||
// 1. log.output.episodeUuid (single episode - CONVERSATION type)
|
||||
// 2. log.output.episodes array (multiple episodes - DOCUMENT type)
|
||||
const logs = await prisma.ingestionQueue.findMany({
|
||||
where: {
|
||||
OR: [
|
||||
{
|
||||
output: {
|
||||
path: ["episodeUuid"],
|
||||
equals: episodeUuid,
|
||||
},
|
||||
},
|
||||
{
|
||||
output: {
|
||||
path: ["episodes"],
|
||||
array_contains: episodeUuid,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: "desc",
|
||||
},
|
||||
take: 1,
|
||||
});
|
||||
|
||||
return logs[0] || null;
|
||||
};
|
||||
|
||||
export const deleteIngestionQueue = async (id: string) => {
|
||||
return await prisma.ingestionQueue.delete({
|
||||
where: {
|
||||
|
||||
@ -33,14 +33,6 @@ export async function getUser(request: Request) {
|
||||
throw await logout(request);
|
||||
}
|
||||
|
||||
export async function getUserRemainingCount(request: Request) {
|
||||
const userId = await getUserId(request);
|
||||
if (userId === undefined) return null;
|
||||
|
||||
const userUsage = await getUserLeftCredits(userId);
|
||||
if (userUsage) return userUsage;
|
||||
}
|
||||
|
||||
export async function requireUserId(request: Request, redirectTo?: string) {
|
||||
const userId = await getUserId(request);
|
||||
if (!userId) {
|
||||
@ -71,6 +63,7 @@ export async function requireUser(request: Request) {
|
||||
confirmedBasicDetails: user.confirmedBasicDetails,
|
||||
onboardingComplete: user.onboardingComplete,
|
||||
isImpersonating: !!impersonationId,
|
||||
workspaceId: user.Workspace?.id,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -3,20 +3,18 @@ import {
|
||||
type SpaceNode,
|
||||
type CreateSpaceParams,
|
||||
type UpdateSpaceParams,
|
||||
type SpaceAssignmentResult,
|
||||
} from "@core/types";
|
||||
import { type Space } from "@prisma/client";
|
||||
|
||||
import { triggerSpaceAssignment } from "~/trigger/spaces/space-assignment";
|
||||
import {
|
||||
assignEpisodesToSpace,
|
||||
assignStatementsToSpace,
|
||||
createSpace,
|
||||
deleteSpace,
|
||||
getSpace,
|
||||
getSpaceStatements,
|
||||
initializeStatementSpaceIds,
|
||||
removeStatementsFromSpace,
|
||||
getSpaceEpisodeCount,
|
||||
getSpaceEpisodes,
|
||||
removeEpisodesFromSpace,
|
||||
updateSpace,
|
||||
} from "./graphModels/space";
|
||||
import { prisma } from "~/trigger/utils/prisma";
|
||||
@ -278,97 +276,11 @@ export class SpaceService {
|
||||
return resetSpace;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assign statements to a space
|
||||
*/
|
||||
async assignStatementsToSpace(
|
||||
statementIds: string[],
|
||||
spaceId: string,
|
||||
userId: string,
|
||||
): Promise<SpaceAssignmentResult> {
|
||||
logger.info(
|
||||
`Assigning ${statementIds.length} statements to space ${spaceId} for user ${userId}`,
|
||||
);
|
||||
|
||||
// Validate input
|
||||
if (statementIds.length === 0) {
|
||||
throw new Error("No statement IDs provided");
|
||||
}
|
||||
|
||||
if (statementIds.length > 1000) {
|
||||
throw new Error("Too many statements (max 1000 per operation)");
|
||||
}
|
||||
|
||||
const result = await assignStatementsToSpace(statementIds, spaceId, userId);
|
||||
|
||||
if (result.success) {
|
||||
logger.info(
|
||||
`Successfully assigned ${result.statementsUpdated} statements to space ${spaceId}`,
|
||||
);
|
||||
} else {
|
||||
logger.warn(
|
||||
`Failed to assign statements to space ${spaceId}: ${result.error}`,
|
||||
);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove statements from a space
|
||||
*/
|
||||
async removeStatementsFromSpace(
|
||||
statementIds: string[],
|
||||
spaceId: string,
|
||||
userId: string,
|
||||
): Promise<SpaceAssignmentResult> {
|
||||
logger.info(
|
||||
`Removing ${statementIds.length} statements from space ${spaceId} for user ${userId}`,
|
||||
);
|
||||
|
||||
// Validate input
|
||||
if (statementIds.length === 0) {
|
||||
throw new Error("No statement IDs provided");
|
||||
}
|
||||
|
||||
if (statementIds.length > 1000) {
|
||||
throw new Error("Too many statements (max 1000 per operation)");
|
||||
}
|
||||
|
||||
const result = await removeStatementsFromSpace(
|
||||
statementIds,
|
||||
spaceId,
|
||||
userId,
|
||||
);
|
||||
|
||||
if (result.success) {
|
||||
logger.info(
|
||||
`Successfully removed ${result.statementsUpdated} statements from space ${spaceId}`,
|
||||
);
|
||||
} else {
|
||||
logger.warn(
|
||||
`Failed to remove statements from space ${spaceId}: ${result.error}`,
|
||||
);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all statements in a space
|
||||
* @deprecated Use getSpaceEpisodes instead - spaces now work with episodes
|
||||
*/
|
||||
async getSpaceStatements(spaceId: string, userId: string) {
|
||||
logger.info(`Fetching statements for space ${spaceId} for user ${userId}`);
|
||||
return await getSpaceStatements(spaceId, userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all episodes in a space
|
||||
*/
|
||||
async getSpaceEpisodes(spaceId: string, userId: string) {
|
||||
logger.info(`Fetching episodes for space ${spaceId} for user ${userId}`);
|
||||
const { getSpaceEpisodes } = await import("./graphModels/space");
|
||||
return await getSpaceEpisodes(spaceId, userId);
|
||||
}
|
||||
|
||||
@ -384,7 +296,7 @@ export class SpaceService {
|
||||
`Assigning ${episodeIds.length} episodes to space ${spaceId} for user ${userId}`,
|
||||
);
|
||||
|
||||
await assignEpisodesToSpace(episodeIds,spaceId, userId);
|
||||
await assignEpisodesToSpace(episodeIds, spaceId, userId);
|
||||
|
||||
logger.info(
|
||||
`Successfully assigned ${episodeIds.length} episodes to space ${spaceId}`,
|
||||
@ -403,7 +315,7 @@ export class SpaceService {
|
||||
`Removing ${episodeIds.length} episodes from space ${spaceId} for user ${userId}`,
|
||||
);
|
||||
|
||||
await this.removeEpisodesFromSpace(episodeIds, spaceId, userId);
|
||||
await removeEpisodesFromSpace(episodeIds, spaceId, userId);
|
||||
|
||||
logger.info(
|
||||
`Successfully removed ${episodeIds.length} episodes from space ${spaceId}`,
|
||||
@ -432,49 +344,6 @@ export class SpaceService {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get spaces that contain specific statements
|
||||
*/
|
||||
async getSpacesForStatements(
|
||||
statementIds: string[],
|
||||
userId: string,
|
||||
): Promise<{ statementId: string; spaces: Space[] }[]> {
|
||||
const userSpaces = await this.getUserSpaces(userId);
|
||||
const result: { statementId: string; spaces: Space[] }[] = [];
|
||||
|
||||
for (const statementId of statementIds) {
|
||||
const spacesContainingStatement = [];
|
||||
|
||||
for (const space of userSpaces) {
|
||||
const statements = await this.getSpaceStatements(space.id, userId);
|
||||
if (statements.some((stmt) => stmt.uuid === statementId)) {
|
||||
spacesContainingStatement.push(space);
|
||||
}
|
||||
}
|
||||
|
||||
result.push({
|
||||
statementId,
|
||||
spaces: spacesContainingStatement,
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize spaceIds for existing statements (migration utility)
|
||||
*/
|
||||
async initializeSpaceIds(userId?: string): Promise<number> {
|
||||
logger.info(
|
||||
`Initializing spaceIds for ${userId ? `user ${userId}` : "all users"}`,
|
||||
);
|
||||
|
||||
const updatedCount = await initializeStatementSpaceIds(userId);
|
||||
|
||||
logger.info(`Initialized spaceIds for ${updatedCount} statements`);
|
||||
return updatedCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate space access
|
||||
*/
|
||||
@ -482,41 +351,4 @@ export class SpaceService {
|
||||
const space = await this.getSpace(spaceId, userId);
|
||||
return space !== null && space.isActive;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bulk assign statements to multiple spaces
|
||||
*/
|
||||
async bulkAssignStatements(
|
||||
statementIds: string[],
|
||||
spaceIds: string[],
|
||||
userId: string,
|
||||
): Promise<{ spaceId: string; result: SpaceAssignmentResult }[]> {
|
||||
logger.info(
|
||||
`Bulk assigning ${statementIds.length} statements to ${spaceIds.length} spaces for user ${userId}`,
|
||||
);
|
||||
|
||||
const results: { spaceId: string; result: SpaceAssignmentResult }[] = [];
|
||||
|
||||
for (const spaceId of spaceIds) {
|
||||
try {
|
||||
const result = await this.assignStatementsToSpace(
|
||||
statementIds,
|
||||
spaceId,
|
||||
userId,
|
||||
);
|
||||
results.push({ spaceId, result });
|
||||
} catch (error) {
|
||||
results.push({
|
||||
spaceId,
|
||||
result: {
|
||||
success: false,
|
||||
statementsUpdated: 0,
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
}
|
||||
|
||||
@ -125,6 +125,7 @@
|
||||
"react-dom": "^18.2.0",
|
||||
"react-markdown": "10.1.0",
|
||||
"react-resizable-panels": "^1.0.9",
|
||||
"react-hotkeys-hook": "^4.5.0",
|
||||
"react-virtualized": "^9.22.6",
|
||||
"remix-auth": "^4.2.0",
|
||||
"remix-auth-oauth2": "^3.4.1",
|
||||
@ -135,6 +136,7 @@
|
||||
"stripe": "19.0.0",
|
||||
"simple-oauth2": "^5.1.0",
|
||||
"tailwind-merge": "^2.6.0",
|
||||
"tiptap-markdown": "0.9.0",
|
||||
"tailwind-scrollbar-hide": "^2.0.0",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"tailwindcss-textshadow": "^2.1.3",
|
||||
|
||||
@ -39,6 +39,7 @@ export interface EpisodicNode {
|
||||
sessionId?: string;
|
||||
recallCount?: number;
|
||||
chunkIndex?: number; // Index of this chunk within the document
|
||||
spaceIds?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
53
pnpm-lock.yaml
generated
53
pnpm-lock.yaml
generated
@ -607,6 +607,9 @@ importers:
|
||||
react-dom:
|
||||
specifier: ^18.2.0
|
||||
version: 18.3.1(react@18.3.1)
|
||||
react-hotkeys-hook:
|
||||
specifier: ^4.5.0
|
||||
version: 4.6.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
react-markdown:
|
||||
specifier: 10.1.0
|
||||
version: 10.1.0(@types/react@18.2.69)(react@18.3.1)
|
||||
@ -655,6 +658,9 @@ importers:
|
||||
tiny-invariant:
|
||||
specifier: ^1.3.1
|
||||
version: 1.3.3
|
||||
tiptap-markdown:
|
||||
specifier: 0.9.0
|
||||
version: 0.9.0(@tiptap/core@2.25.0(@tiptap/pm@2.25.0))
|
||||
zod:
|
||||
specifier: 3.25.76
|
||||
version: 3.25.76
|
||||
@ -5741,9 +5747,15 @@ packages:
|
||||
'@types/json5@0.0.29':
|
||||
resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==}
|
||||
|
||||
'@types/linkify-it@3.0.5':
|
||||
resolution: {integrity: sha512-yg6E+u0/+Zjva+buc3EIb+29XEg4wltq7cSmd4Uc2EE/1nUVmxyzpX6gUXD0V8jIrG0r7YeOGVIbYRkxeooCtw==}
|
||||
|
||||
'@types/linkify-it@5.0.0':
|
||||
resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==}
|
||||
|
||||
'@types/markdown-it@13.0.9':
|
||||
resolution: {integrity: sha512-1XPwR0+MgXLWfTn9gCsZ55AHOKW1WN+P9vr0PaQh5aerR9LLQXUbjfEAFhjmEmyoYFWAyuN2Mqkn40MZ4ukjBw==}
|
||||
|
||||
'@types/markdown-it@14.1.2':
|
||||
resolution: {integrity: sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==}
|
||||
|
||||
@ -5753,6 +5765,9 @@ packages:
|
||||
'@types/mdast@4.0.4':
|
||||
resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==}
|
||||
|
||||
'@types/mdurl@1.0.5':
|
||||
resolution: {integrity: sha512-6L6VymKTzYSrEf4Nev4Xa1LCHKrlTlYCBMTlQKFuddo1CvQcE52I0mwfOJayueUC7MJuXOeHTcIU683lzd0cUA==}
|
||||
|
||||
'@types/mdurl@2.0.0':
|
||||
resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==}
|
||||
|
||||
@ -9066,6 +9081,9 @@ packages:
|
||||
resolution: {integrity: sha512-WWC0ZuMzCyDHYCasEGs4IPvLyTGftYwh6wIEOULOF0HXcqZlhwRzrK0w2VUlxWA98xnvb/jszw4ZSkJ6ADpM6Q==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
markdown-it-task-lists@2.1.1:
|
||||
resolution: {integrity: sha512-TxFAc76Jnhb2OUu+n3yz9RMu4CwGfaT788br6HhEDlvWfdeJcLUsxk1Hgw2yJio0OXsxv7pyIPmvECY7bMbluA==}
|
||||
|
||||
markdown-it@14.1.0:
|
||||
resolution: {integrity: sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==}
|
||||
hasBin: true
|
||||
@ -10584,6 +10602,12 @@ packages:
|
||||
engines: {node: '>=18.0.0'}
|
||||
hasBin: true
|
||||
|
||||
react-hotkeys-hook@4.6.2:
|
||||
resolution: {integrity: sha512-FmP+ZriY3EG59Ug/lxNfrObCnW9xQShgk7Nb83+CkpfkcCpfS95ydv+E9JuXA5cp8KtskU7LGlIARpkc92X22Q==}
|
||||
peerDependencies:
|
||||
react: '>=16.8.1'
|
||||
react-dom: '>=16.8.1'
|
||||
|
||||
react-is@16.13.1:
|
||||
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
|
||||
|
||||
@ -11556,6 +11580,11 @@ packages:
|
||||
tiptap-extension-global-drag-handle@0.1.18:
|
||||
resolution: {integrity: sha512-jwFuy1K8DP3a4bFy76Hpc63w1Sil0B7uZ3mvhQomVvUFCU787Lg2FowNhn7NFzeyok761qY2VG+PZ/FDthWUdg==}
|
||||
|
||||
tiptap-markdown@0.9.0:
|
||||
resolution: {integrity: sha512-dKLQ9iiuGNgrlGVjrNauF/UBzWu4LYOx5pkD0jNkmQt/GOwfCJsBuzZTsf1jZ204ANHOm572mZ9PYvGh1S7tpQ==}
|
||||
peerDependencies:
|
||||
'@tiptap/core': ^3.0.1
|
||||
|
||||
tmp@0.0.33:
|
||||
resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==}
|
||||
engines: {node: '>=0.6.0'}
|
||||
@ -18728,8 +18757,15 @@ snapshots:
|
||||
|
||||
'@types/json5@0.0.29': {}
|
||||
|
||||
'@types/linkify-it@3.0.5': {}
|
||||
|
||||
'@types/linkify-it@5.0.0': {}
|
||||
|
||||
'@types/markdown-it@13.0.9':
|
||||
dependencies:
|
||||
'@types/linkify-it': 3.0.5
|
||||
'@types/mdurl': 1.0.5
|
||||
|
||||
'@types/markdown-it@14.1.2':
|
||||
dependencies:
|
||||
'@types/linkify-it': 5.0.0
|
||||
@ -18743,6 +18779,8 @@ snapshots:
|
||||
dependencies:
|
||||
'@types/unist': 3.0.3
|
||||
|
||||
'@types/mdurl@1.0.5': {}
|
||||
|
||||
'@types/mdurl@2.0.0': {}
|
||||
|
||||
'@types/mdx@2.0.13': {}
|
||||
@ -22608,6 +22646,8 @@ snapshots:
|
||||
|
||||
markdown-extensions@1.1.1: {}
|
||||
|
||||
markdown-it-task-lists@2.1.1: {}
|
||||
|
||||
markdown-it@14.1.0:
|
||||
dependencies:
|
||||
argparse: 2.0.1
|
||||
@ -24536,6 +24576,11 @@ snapshots:
|
||||
- utf-8-validate
|
||||
- webpack-cli
|
||||
|
||||
react-hotkeys-hook@4.6.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
|
||||
dependencies:
|
||||
react: 18.3.1
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
|
||||
react-is@16.13.1: {}
|
||||
|
||||
react-is@17.0.2: {}
|
||||
@ -25790,6 +25835,14 @@ snapshots:
|
||||
|
||||
tiptap-extension-global-drag-handle@0.1.18: {}
|
||||
|
||||
tiptap-markdown@0.9.0(@tiptap/core@2.25.0(@tiptap/pm@2.25.0)):
|
||||
dependencies:
|
||||
'@tiptap/core': 2.25.0(@tiptap/pm@2.25.0)
|
||||
'@types/markdown-it': 13.0.9
|
||||
markdown-it: 14.1.0
|
||||
markdown-it-task-lists: 2.1.1
|
||||
prosemirror-markdown: 1.13.2
|
||||
|
||||
tmp@0.0.33:
|
||||
dependencies:
|
||||
os-tmpdir: 1.0.2
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user