mirror of
https://github.com/eliasstepanik/core.git
synced 2026-01-11 09:58:28 +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 TableRow from "@tiptap/extension-table-row";
|
||||||
import { all, createLowlight } from "lowlight";
|
import { all, createLowlight } from "lowlight";
|
||||||
import { mergeAttributes, type Extension } from "@tiptap/react";
|
import { mergeAttributes, type Extension } from "@tiptap/react";
|
||||||
|
import { Markdown } from "tiptap-markdown";
|
||||||
|
|
||||||
// create a lowlight instance with all languages loaded
|
// create a lowlight instance with all languages loaded
|
||||||
export const lowlight = createLowlight(all);
|
export const lowlight = createLowlight(all);
|
||||||
@ -136,4 +137,5 @@ export const extensionsForConversation = [
|
|||||||
CodeBlockLowlight.configure({
|
CodeBlockLowlight.configure({
|
||||||
lowlight,
|
lowlight,
|
||||||
}),
|
}),
|
||||||
|
Markdown,
|
||||||
];
|
];
|
||||||
|
|||||||
@ -83,10 +83,14 @@ export const GraphClusteringVisualization = forwardRef<
|
|||||||
filtered = filtered.filter((triplet) => {
|
filtered = filtered.filter((triplet) => {
|
||||||
const sourceMatches =
|
const sourceMatches =
|
||||||
isEpisodeNode(triplet.sourceNode) &&
|
isEpisodeNode(triplet.sourceNode) &&
|
||||||
triplet.sourceNode.attributes?.content?.toLowerCase().includes(query);
|
triplet.sourceNode.attributes?.content
|
||||||
|
?.toLowerCase()
|
||||||
|
.includes(query);
|
||||||
const targetMatches =
|
const targetMatches =
|
||||||
isEpisodeNode(triplet.targetNode) &&
|
isEpisodeNode(triplet.targetNode) &&
|
||||||
triplet.targetNode.attributes?.content?.toLowerCase().includes(query);
|
triplet.targetNode.attributes?.content
|
||||||
|
?.toLowerCase()
|
||||||
|
.includes(query);
|
||||||
|
|
||||||
return sourceMatches || targetMatches;
|
return sourceMatches || targetMatches;
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { useState, useEffect, type ReactNode } from "react";
|
import { useState, useEffect, type ReactNode } from "react";
|
||||||
import { useFetcher } from "@remix-run/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 { Badge, BadgeColor } from "../ui/badge";
|
||||||
import { type LogItem } from "~/hooks/use-logs";
|
import { type LogItem } from "~/hooks/use-logs";
|
||||||
import Markdown from "react-markdown";
|
import Markdown from "react-markdown";
|
||||||
@ -8,6 +8,7 @@ import { getIconForAuthorise } from "../icon-utils";
|
|||||||
import { cn, formatString } from "~/lib/utils";
|
import { cn, formatString } from "~/lib/utils";
|
||||||
import { getStatusColor } from "./utils";
|
import { getStatusColor } from "./utils";
|
||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
|
import { SpaceDropdown } from "../spaces/space-dropdown";
|
||||||
|
|
||||||
interface LogDetailsProps {
|
interface LogDetailsProps {
|
||||||
log: LogItem;
|
log: LogItem;
|
||||||
@ -33,13 +34,13 @@ function PropertyItem({
|
|||||||
if (!value) return null;
|
if (!value) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center py-1">
|
<div className="flex items-center py-1 !text-base">
|
||||||
<span className="text-muted-foreground min-w-[160px]">{label}</span>
|
<span className="text-muted-foreground min-w-[120px]">{label}</span>
|
||||||
|
|
||||||
{variant === "status" ? (
|
{variant === "status" ? (
|
||||||
<Badge
|
<Badge
|
||||||
className={cn(
|
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,
|
className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@ -49,7 +50,13 @@ function PropertyItem({
|
|||||||
{value}
|
{value}
|
||||||
</Badge>
|
</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}
|
{icon}
|
||||||
{value}
|
{value}
|
||||||
</Badge>
|
</Badge>
|
||||||
@ -73,10 +80,10 @@ interface EpisodeFactsResponse {
|
|||||||
|
|
||||||
function getStatusValue(status: string) {
|
function getStatusValue(status: string) {
|
||||||
if (status === "PENDING") {
|
if (status === "PENDING") {
|
||||||
return "In Queue";
|
return formatString("IN QUEUE");
|
||||||
}
|
}
|
||||||
|
|
||||||
return status;
|
return formatString(status);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function LogDetails({ log }: LogDetailsProps) {
|
export function LogDetails({ log }: LogDetailsProps) {
|
||||||
@ -113,6 +120,9 @@ export function LogDetails({ log }: LogDetailsProps) {
|
|||||||
} else if (log.episodeUUID) {
|
} else if (log.episodeUUID) {
|
||||||
setFactsLoading(true);
|
setFactsLoading(true);
|
||||||
fetcher.load(`/api/v1/episodes/${log.episodeUUID}/facts`);
|
fetcher.load(`/api/v1/episodes/${log.episodeUUID}/facts`);
|
||||||
|
} else {
|
||||||
|
setFacts([]);
|
||||||
|
setInvalidFacts([]);
|
||||||
}
|
}
|
||||||
}, [log.episodeUUID, log.data?.type, log.data?.episodes, facts.length]);
|
}, [log.episodeUUID, log.data?.type, log.data?.episodes, facts.length]);
|
||||||
|
|
||||||
@ -129,41 +139,8 @@ export function LogDetails({ log }: LogDetailsProps) {
|
|||||||
return (
|
return (
|
||||||
<div className="flex h-full w-full flex-col items-center overflow-auto">
|
<div className="flex h-full w-full flex-col items-center overflow-auto">
|
||||||
<div className="max-w-4xl">
|
<div className="max-w-4xl">
|
||||||
<div className="px-4 pt-4">
|
<div className="mt-5 mb-5 px-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="space-y-1">
|
<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
|
<PropertyItem
|
||||||
label="Session Id"
|
label="Session Id"
|
||||||
value={log.data?.sessionId?.toLowerCase()}
|
value={log.data?.sessionId?.toLowerCase()}
|
||||||
@ -174,6 +151,13 @@ export function LogDetails({ log }: LogDetailsProps) {
|
|||||||
value={formatString(
|
value={formatString(
|
||||||
log.data?.type ? log.data.type.toLowerCase() : "conversation",
|
log.data?.type ? log.data.type.toLowerCase() : "conversation",
|
||||||
)}
|
)}
|
||||||
|
icon={
|
||||||
|
log.data?.type === "CONVERSATION" ? (
|
||||||
|
<MessageSquare size={16} />
|
||||||
|
) : (
|
||||||
|
<File size={16} />
|
||||||
|
)
|
||||||
|
}
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
/>
|
/>
|
||||||
<PropertyItem
|
<PropertyItem
|
||||||
@ -192,15 +176,28 @@ export function LogDetails({ log }: LogDetailsProps) {
|
|||||||
variant="status"
|
variant="status"
|
||||||
statusColor={log.status && getStatusColor(log.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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Error Details */}
|
{/* Error Details */}
|
||||||
{log.error && (
|
{log.error && (
|
||||||
<div className="mb-6 px-4">
|
<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="bg-destructive/10 rounded-md p-3">
|
||||||
<div className="flex items-start gap-2 text-red-600">
|
<div className="flex items-start gap-2 text-red-600">
|
||||||
<AlertCircle className="mt-0.5 h-4 w-4 flex-shrink-0" />
|
<AlertCircle className="mt-0.5 h-4 w-4 flex-shrink-0" />
|
||||||
@ -212,21 +209,63 @@ export function LogDetails({ log }: LogDetailsProps) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex flex-col items-center p-4 pt-0">
|
{log.data?.type === "CONVERSATION" && (
|
||||||
<div className="mb-2 flex w-full items-center justify-between">
|
<div className="flex flex-col items-center p-4 pt-0">
|
||||||
<span>Content</span>
|
{/* Log Content */}
|
||||||
</div>
|
<div className="mb-4 w-full break-words whitespace-pre-wrap">
|
||||||
{/* Log Content */}
|
<div className="rounded-md">
|
||||||
<div className="mb-4 w-full text-sm break-words whitespace-pre-wrap">
|
<Markdown>{log.ingestText}</Markdown>
|
||||||
<div className="rounded-md">
|
</div>
|
||||||
<Markdown>{log.ingestText}</Markdown>
|
|
||||||
</div>
|
</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 */}
|
{/* Episode Facts */}
|
||||||
<div className="mb-6 px-4">
|
<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>
|
<span>Facts</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-md">
|
<div className="rounded-md">
|
||||||
|
|||||||
@ -41,7 +41,6 @@ export const LogOptions = ({ id }: LogOptionsProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log(deleteFetcher.state, deleteFetcher.data);
|
|
||||||
if (deleteFetcher.state === "idle" && deleteFetcher.data?.success) {
|
if (deleteFetcher.state === "idle" && deleteFetcher.data?.success) {
|
||||||
navigate(`/home/inbox`);
|
navigate(`/home/inbox`);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import { type LogItem } from "~/hooks/use-logs";
|
|||||||
import { getIconForAuthorise } from "../icon-utils";
|
import { getIconForAuthorise } from "../icon-utils";
|
||||||
import { useNavigate, useParams } from "@remix-run/react";
|
import { useNavigate, useParams } from "@remix-run/react";
|
||||||
import { getStatusColor, getStatusValue } from "./utils";
|
import { getStatusColor, getStatusValue } from "./utils";
|
||||||
|
import { File, MessageSquare } from "lucide-react";
|
||||||
|
|
||||||
interface LogTextCollapseProps {
|
interface LogTextCollapseProps {
|
||||||
text?: string;
|
text?: string;
|
||||||
@ -49,9 +50,13 @@ export function LogTextCollapse({ text, log }: LogTextCollapseProps) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getIngestType = (log: LogItem) => {
|
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 (
|
return (
|
||||||
@ -100,7 +105,7 @@ export function LogTextCollapse({ text, log }: LogTextCollapseProps) {
|
|||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<Badge
|
<Badge
|
||||||
className={cn(
|
className={cn(
|
||||||
"!bg-grayAlpha-100 text-muted-foreground rounded text-xs",
|
"text-muted-foreground rounded !bg-transparent text-xs",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{getIngestType(log)}
|
{getIngestType(log)}
|
||||||
|
|||||||
@ -22,5 +22,5 @@ export function getStatusValue(status: string) {
|
|||||||
return formatString("In Queue");
|
return formatString("In Queue");
|
||||||
}
|
}
|
||||||
|
|
||||||
return status;
|
return formatString(status);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import {
|
|||||||
import { type LogItem } from "~/hooks/use-logs";
|
import { type LogItem } from "~/hooks/use-logs";
|
||||||
import { ScrollManagedList } from "../virtualized-list";
|
import { ScrollManagedList } from "../virtualized-list";
|
||||||
import { LogTextCollapse } from "./log-text-collapse";
|
import { LogTextCollapse } from "./log-text-collapse";
|
||||||
|
import { LoaderCircle } from "lucide-react";
|
||||||
|
|
||||||
interface VirtualLogsListProps {
|
interface VirtualLogsListProps {
|
||||||
logs: LogItem[];
|
logs: LogItem[];
|
||||||
@ -139,7 +140,7 @@ export function VirtualLogsList({
|
|||||||
|
|
||||||
{isLoading && (
|
{isLoading && (
|
||||||
<div className="text-muted-foreground p-4 text-center text-sm">
|
<div className="text-muted-foreground p-4 text-center text-sm">
|
||||||
Loading more logs...
|
<LoaderCircle size={18} className="mr-1 animate-spin" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -139,6 +139,7 @@ export default function OnboardingQuestionComponent({
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="xl"
|
size="xl"
|
||||||
onClick={onPrevious}
|
onClick={onPrevious}
|
||||||
|
disabled={loading}
|
||||||
className="rounded-lg px-4 py-2"
|
className="rounded-lg px-4 py-2"
|
||||||
>
|
>
|
||||||
Previous
|
Previous
|
||||||
@ -151,7 +152,7 @@ export default function OnboardingQuestionComponent({
|
|||||||
size="xl"
|
size="xl"
|
||||||
onClick={onNext}
|
onClick={onNext}
|
||||||
isLoading={!!loading}
|
isLoading={!!loading}
|
||||||
disabled={!isValid()}
|
disabled={!isValid() || loading}
|
||||||
className="rounded-lg px-4 py-2"
|
className="rounded-lg px-4 py-2"
|
||||||
>
|
>
|
||||||
{isLast ? "Complete Profile" : "Continue"}
|
{isLast ? "Complete Profile" : "Continue"}
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
import { useHotkeys } from "react-hotkeys-hook";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Sidebar,
|
Sidebar,
|
||||||
@ -12,14 +13,20 @@ import {
|
|||||||
Columns3,
|
Columns3,
|
||||||
Inbox,
|
Inbox,
|
||||||
LayoutGrid,
|
LayoutGrid,
|
||||||
|
LoaderCircle,
|
||||||
MessageSquare,
|
MessageSquare,
|
||||||
Network,
|
Network,
|
||||||
|
Plus,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { NavMain } from "./nav-main";
|
import { NavMain } from "./nav-main";
|
||||||
import { useUser } from "~/hooks/useUser";
|
import { useUser } from "~/hooks/useUser";
|
||||||
import { NavUser } from "./nav-user";
|
import { NavUser } from "./nav-user";
|
||||||
import Logo from "../logo/logo";
|
import Logo from "../logo/logo";
|
||||||
import { ConversationList } from "../conversation";
|
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 = {
|
const data = {
|
||||||
navMain: [
|
navMain: [
|
||||||
@ -41,7 +48,7 @@ const data = {
|
|||||||
{
|
{
|
||||||
title: "Spaces",
|
title: "Spaces",
|
||||||
url: "/home/space",
|
url: "/home/space",
|
||||||
icon: Columns3,
|
icon: Project,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Integrations",
|
title: "Integrations",
|
||||||
@ -54,33 +61,57 @@ const data = {
|
|||||||
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
||||||
const user = useUser();
|
const user = useUser();
|
||||||
|
|
||||||
return (
|
const [showAddMemory, setShowAddMemory] = React.useState(false);
|
||||||
<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>
|
|
||||||
|
|
||||||
<SidebarFooter className="px-2">
|
// Open command bar with Meta+K (Cmd+K on Mac, Ctrl+K on Windows/Linux)
|
||||||
<NavUser user={user} />
|
useHotkeys("meta+k", (e) => {
|
||||||
</SidebarFooter>
|
e.preventDefault();
|
||||||
</Sidebar>
|
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>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => {
|
||||||
|
navigate("/settings/billing");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div>{user.availableCredits} credits</div>
|
||||||
|
</Button>
|
||||||
</SidebarMenuItem>
|
</SidebarMenuItem>
|
||||||
</SidebarMenu>
|
</SidebarMenu>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -17,8 +17,8 @@ interface SpaceCardProps {
|
|||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
autoMode: boolean;
|
autoMode: boolean;
|
||||||
statementCount: number | null;
|
|
||||||
summary: string | null;
|
summary: string | null;
|
||||||
|
contextCount?: number | null;
|
||||||
themes?: string[];
|
themes?: string[];
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -46,13 +46,17 @@ export function SpaceCard({ space }: SpaceCardProps) {
|
|||||||
</div>
|
</div>
|
||||||
<CardTitle className="text-base">{space.name}</CardTitle>
|
<CardTitle className="text-base">{space.name}</CardTitle>
|
||||||
<CardDescription className="line-clamp-2 text-xs">
|
<CardDescription className="line-clamp-2 text-xs">
|
||||||
{space.description || space.summary || "Knowledge space"}
|
<p
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: space.description || space.summary || "Knowledge space",
|
||||||
|
}}
|
||||||
|
></p>
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
<div className="text-muted-foreground mt-2 flex items-center justify-between text-xs">
|
<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>
|
<div>
|
||||||
{space.statementCount} fact
|
{space.contextCount} episode
|
||||||
{space.statementCount !== 1 ? "s" : ""}
|
{space.contextCount !== 1 ? "s" : ""}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</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 { Badge } from "~/components/ui/badge";
|
||||||
import type { StatementNode } from "@core/types";
|
import type { StatementNode } from "@core/types";
|
||||||
import { cn } from "~/lib/utils";
|
import { cn } from "~/lib/utils";
|
||||||
|
import { useNavigate } from "@remix-run/react";
|
||||||
|
import Markdown from "react-markdown";
|
||||||
|
|
||||||
interface SpaceFactCardProps {
|
export interface Episode {
|
||||||
fact: StatementNode;
|
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 formatDate = (date: Date | string) => {
|
||||||
const d = new Date(date);
|
const d = new Date(date);
|
||||||
return d.toLocaleDateString("en-US", {
|
return d.toLocaleDateString("en-US", {
|
||||||
@ -17,18 +32,20 @@ export function SpaceFactCard({ fact }: SpaceFactCardProps) {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const displayText = fact.fact;
|
const displayText = episode.originalContent;
|
||||||
|
|
||||||
const recallCount =
|
const onClick = () => {
|
||||||
(fact.recallCount?.high ?? 0) + (fact.recallCount?.low ?? 0);
|
navigate(`/home/inbox/${episode.logId}`);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
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
|
<div
|
||||||
className={cn(
|
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
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
@ -37,19 +54,13 @@ export function SpaceFactCard({ fact }: SpaceFactCardProps) {
|
|||||||
>
|
>
|
||||||
<div className="flex w-full items-center justify-between gap-4">
|
<div className="flex w-full items-center justify-between gap-4">
|
||||||
<div className="inline-flex min-h-[24px] min-w-[0px] shrink 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>
|
<Markdown>{displayText}</Markdown>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-muted-foreground flex shrink-0 items-center justify-end gap-2 text-xs">
|
<div className="text-muted-foreground flex shrink-0 items-center justify-end gap-2 text-xs">
|
||||||
{!!recallCount && <span>Recalled: {recallCount} times</span>}
|
|
||||||
<Badge variant="secondary" className="rounded text-xs">
|
<Badge variant="secondary" className="rounded text-xs">
|
||||||
<Calendar className="h-3 w-3" />
|
<Calendar className="h-3 w-3" />
|
||||||
{formatDate(fact.validAt)}
|
{formatDate(episode.validAt)}
|
||||||
</Badge>
|
</Badge>
|
||||||
{fact.invalidAt && (
|
|
||||||
<Badge variant="destructive" className="rounded text-xs">
|
|
||||||
Invalid since {formatDate(fact.invalidAt)}
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -9,7 +9,7 @@ import {
|
|||||||
} from "~/components/ui/popover";
|
} from "~/components/ui/popover";
|
||||||
import { Badge } from "~/components/ui/badge";
|
import { Badge } from "~/components/ui/badge";
|
||||||
|
|
||||||
interface SpaceFactsFiltersProps {
|
interface SpaceEpisodesFiltersProps {
|
||||||
selectedValidDate?: string;
|
selectedValidDate?: string;
|
||||||
selectedSpaceFilter?: string;
|
selectedSpaceFilter?: string;
|
||||||
onValidDateChange: (date?: string) => void;
|
onValidDateChange: (date?: string) => void;
|
||||||
@ -22,34 +22,24 @@ const validDateOptions = [
|
|||||||
{ value: "last_6_months", label: "Last 6 Months" },
|
{ value: "last_6_months", label: "Last 6 Months" },
|
||||||
];
|
];
|
||||||
|
|
||||||
const spaceFilterOptions = [
|
type FilterStep = "main" | "validDate";
|
||||||
{ value: "active", label: "Active Facts" },
|
|
||||||
{ value: "archived", label: "Archived Facts" },
|
|
||||||
{ value: "all", label: "All Facts" },
|
|
||||||
];
|
|
||||||
|
|
||||||
type FilterStep = "main" | "validDate" | "spaceFilter";
|
export function SpaceEpisodesFilters({
|
||||||
|
|
||||||
export function SpaceFactsFilters({
|
|
||||||
selectedValidDate,
|
selectedValidDate,
|
||||||
selectedSpaceFilter,
|
selectedSpaceFilter,
|
||||||
onValidDateChange,
|
onValidDateChange,
|
||||||
onSpaceFilterChange,
|
}: SpaceEpisodesFiltersProps) {
|
||||||
}: SpaceFactsFiltersProps) {
|
|
||||||
const [popoverOpen, setPopoverOpen] = useState(false);
|
const [popoverOpen, setPopoverOpen] = useState(false);
|
||||||
const [step, setStep] = useState<FilterStep>("main");
|
const [step, setStep] = useState<FilterStep>("main");
|
||||||
|
|
||||||
const selectedValidDateLabel = validDateOptions.find(
|
const selectedValidDateLabel = validDateOptions.find(
|
||||||
(d) => d.value === selectedValidDate,
|
(d) => d.value === selectedValidDate,
|
||||||
)?.label;
|
)?.label;
|
||||||
const selectedSpaceFilterLabel = spaceFilterOptions.find(
|
|
||||||
(f) => f.value === selectedSpaceFilter,
|
|
||||||
)?.label;
|
|
||||||
|
|
||||||
const hasFilters = selectedValidDate || selectedSpaceFilter;
|
const hasFilters = selectedValidDate || selectedSpaceFilter;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mb-2 flex w-full items-center justify-start gap-2 px-5">
|
<>
|
||||||
<Popover
|
<Popover
|
||||||
open={popoverOpen}
|
open={popoverOpen}
|
||||||
onOpenChange={(open) => {
|
onOpenChange={(open) => {
|
||||||
@ -79,13 +69,6 @@ export function SpaceFactsFilters({
|
|||||||
>
|
>
|
||||||
Valid Date
|
Valid Date
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
className="justify-start"
|
|
||||||
onClick={() => setStep("spaceFilter")}
|
|
||||||
>
|
|
||||||
Status
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@ -122,40 +105,6 @@ export function SpaceFactsFilters({
|
|||||||
))}
|
))}
|
||||||
</div>
|
</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>
|
</PopoverContent>
|
||||||
</PopoverPortal>
|
</PopoverPortal>
|
||||||
</Popover>
|
</Popover>
|
||||||
@ -172,17 +121,8 @@ export function SpaceFactsFilters({
|
|||||||
/>
|
/>
|
||||||
</Badge>
|
</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>
|
||||||
)}
|
)}
|
||||||
</div>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -9,25 +9,24 @@ import {
|
|||||||
} from "react-virtualized";
|
} from "react-virtualized";
|
||||||
import { Database } from "lucide-react";
|
import { Database } from "lucide-react";
|
||||||
import { Card, CardContent } from "~/components/ui/card";
|
import { Card, CardContent } from "~/components/ui/card";
|
||||||
import type { StatementNode } from "@core/types";
|
|
||||||
import { ScrollManagedList } from "../virtualized-list";
|
import { ScrollManagedList } from "../virtualized-list";
|
||||||
import { SpaceFactCard } from "./space-fact-card";
|
import { type Episode, SpaceEpisodeCard } from "./space-episode-card";
|
||||||
|
|
||||||
interface SpaceFactsListProps {
|
interface SpaceEpisodesListProps {
|
||||||
facts: any[];
|
episodes: any[];
|
||||||
hasMore: boolean;
|
hasMore: boolean;
|
||||||
loadMore: () => void;
|
loadMore: () => void;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
height?: number;
|
height?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
function FactItemRenderer(
|
function EpisodeItemRenderer(
|
||||||
props: ListRowProps,
|
props: ListRowProps,
|
||||||
facts: StatementNode[],
|
episodes: Episode[],
|
||||||
cache: CellMeasurerCache,
|
cache: CellMeasurerCache,
|
||||||
) {
|
) {
|
||||||
const { index, key, style, parent } = props;
|
const { index, key, style, parent } = props;
|
||||||
const fact = facts[index];
|
const episode = episodes[index];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CellMeasurer
|
<CellMeasurer
|
||||||
@ -38,23 +37,23 @@ function FactItemRenderer(
|
|||||||
rowIndex={index}
|
rowIndex={index}
|
||||||
>
|
>
|
||||||
<div key={key} style={style} className="pb-2">
|
<div key={key} style={style} className="pb-2">
|
||||||
<SpaceFactCard fact={fact} />
|
<SpaceEpisodeCard episode={episode} />
|
||||||
</div>
|
</div>
|
||||||
</CellMeasurer>
|
</CellMeasurer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SpaceFactsList({
|
export function SpaceEpisodesList({
|
||||||
facts,
|
episodes,
|
||||||
hasMore,
|
hasMore,
|
||||||
loadMore,
|
loadMore,
|
||||||
isLoading,
|
isLoading,
|
||||||
}: SpaceFactsListProps) {
|
}: SpaceEpisodesListProps) {
|
||||||
// Create a CellMeasurerCache instance using useRef to prevent recreation
|
// Create a CellMeasurerCache instance using useRef to prevent recreation
|
||||||
const cacheRef = useRef<CellMeasurerCache | null>(null);
|
const cacheRef = useRef<CellMeasurerCache | null>(null);
|
||||||
if (!cacheRef.current) {
|
if (!cacheRef.current) {
|
||||||
cacheRef.current = new CellMeasurerCache({
|
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
|
fixedWidth: true, // Rows have fixed width but dynamic height
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -62,17 +61,17 @@ export function SpaceFactsList({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
cache.clearAll();
|
cache.clearAll();
|
||||||
}, [facts, cache]);
|
}, [episodes, cache]);
|
||||||
|
|
||||||
if (facts.length === 0 && !isLoading) {
|
if (episodes.length === 0 && !isLoading) {
|
||||||
return (
|
return (
|
||||||
<Card className="bg-background-2 w-full">
|
<Card className="bg-background-2 w-full">
|
||||||
<CardContent className="bg-background-2 flex w-full items-center justify-center py-16">
|
<CardContent className="bg-background-2 flex w-full items-center justify-center py-16">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<Database className="text-muted-foreground mx-auto mb-4 h-12 w-12" />
|
<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">
|
<p className="text-muted-foreground">
|
||||||
This space doesn't contain any facts yet.
|
This space doesn't contain any episodes yet.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@ -81,7 +80,7 @@ export function SpaceFactsList({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const isRowLoaded = ({ index }: { index: number }) => {
|
const isRowLoaded = ({ index }: { index: number }) => {
|
||||||
return !!facts[index];
|
return !!episodes[index];
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadMoreRows = async () => {
|
const loadMoreRows = async () => {
|
||||||
@ -92,14 +91,14 @@ export function SpaceFactsList({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const rowRenderer = (props: ListRowProps) => {
|
const rowRenderer = (props: ListRowProps) => {
|
||||||
return FactItemRenderer(props, facts, cache);
|
return EpisodeItemRenderer(props, episodes, cache);
|
||||||
};
|
};
|
||||||
|
|
||||||
const rowHeight = ({ index }: Index) => {
|
const rowHeight = ({ index }: Index) => {
|
||||||
return cache.getHeight(index, 0);
|
return cache.getHeight(index, 0);
|
||||||
};
|
};
|
||||||
|
|
||||||
const itemCount = hasMore ? facts.length + 1 : facts.length;
|
const itemCount = hasMore ? episodes.length + 1 : episodes.length;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full grow overflow-hidden rounded-lg">
|
<div className="h-full grow overflow-hidden rounded-lg">
|
||||||
@ -131,7 +130,7 @@ export function SpaceFactsList({
|
|||||||
|
|
||||||
{isLoading && (
|
{isLoading && (
|
||||||
<div className="text-muted-foreground p-4 text-center text-sm">
|
<div className="text-muted-foreground p-4 text-center text-sm">
|
||||||
Loading more facts...
|
Loading more episodes...
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -9,8 +9,8 @@ interface SpacesGridProps {
|
|||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
autoMode: boolean;
|
autoMode: boolean;
|
||||||
statementCount: number | null;
|
|
||||||
summary: string | null;
|
summary: string | null;
|
||||||
|
contextCount?: number | null;
|
||||||
themes?: string[];
|
themes?: string[];
|
||||||
}>;
|
}>;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -40,7 +40,7 @@ const CommandDialog = ({
|
|||||||
<Dialog {...props}>
|
<Dialog {...props}>
|
||||||
<DialogContent className={cn("overflow-hidden p-0 font-sans")}>
|
<DialogContent className={cn("overflow-hidden p-0 font-sans")}>
|
||||||
<Command
|
<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}
|
{...commandProps}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
@ -141,7 +141,7 @@ const CommandItem = React.forwardRef<
|
|||||||
<CommandPrimitive.Item
|
<CommandPrimitive.Item
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
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,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@ -15,6 +15,8 @@ export interface LogItem {
|
|||||||
activityId?: string;
|
activityId?: string;
|
||||||
episodeUUID?: string;
|
episodeUUID?: string;
|
||||||
data?: any;
|
data?: any;
|
||||||
|
spaceIds?: string[];
|
||||||
|
episodeDetails?: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LogsResponse {
|
export interface LogsResponse {
|
||||||
|
|||||||
@ -5,7 +5,8 @@ import { useChanged } from "./useChanged";
|
|||||||
import { useTypedMatchesData } from "./useTypedMatchData";
|
import { useTypedMatchesData } from "./useTypedMatchData";
|
||||||
|
|
||||||
export interface ExtendedUser extends User {
|
export interface ExtendedUser extends User {
|
||||||
availableCredits?: number;
|
availableCredits: number;
|
||||||
|
totalCredits: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useIsImpersonating(matches?: UIMatch[]) {
|
export function useIsImpersonating(matches?: UIMatch[]) {
|
||||||
@ -23,7 +24,11 @@ export function useOptionalUser(matches?: UIMatch[]): ExtendedUser | undefined {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return routeMatch?.user
|
return routeMatch?.user
|
||||||
? { ...routeMatch?.user, availableCredits: routeMatch?.availableCredits }
|
? {
|
||||||
|
...routeMatch?.user,
|
||||||
|
availableCredits: routeMatch?.availableCredits,
|
||||||
|
totalCredits: routeMatch?.totalCredits,
|
||||||
|
}
|
||||||
: undefined;
|
: undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -145,10 +145,8 @@ export const getClusteredGraphData = async (userId: string) => {
|
|||||||
rel.predicate as predicateLabel,
|
rel.predicate as predicateLabel,
|
||||||
e.uuid as episodeUuid,
|
e.uuid as episodeUuid,
|
||||||
e.content as episodeContent,
|
e.content as episodeContent,
|
||||||
|
e.spaceIds as spaceIds,
|
||||||
s.uuid as statementUuid,
|
s.uuid as statementUuid,
|
||||||
s.spaceIds as spaceIds,
|
|
||||||
s.fact as fact,
|
|
||||||
s.invalidAt as invalidAt,
|
|
||||||
s.validAt as validAt,
|
s.validAt as validAt,
|
||||||
s.createdAt as createdAt`,
|
s.createdAt as createdAt`,
|
||||||
{ userId },
|
{ userId },
|
||||||
@ -169,13 +167,8 @@ export const getClusteredGraphData = async (userId: string) => {
|
|||||||
|
|
||||||
const predicateLabel = record.get("predicateLabel");
|
const predicateLabel = record.get("predicateLabel");
|
||||||
const episodeUuid = record.get("episodeUuid");
|
const episodeUuid = record.get("episodeUuid");
|
||||||
const episodeContent = record.get("episodeContent");
|
|
||||||
const statementUuid = record.get("statementUuid");
|
|
||||||
const clusterIds = record.get("spaceIds");
|
const clusterIds = record.get("spaceIds");
|
||||||
const clusterId = clusterIds ? clusterIds[0] : undefined;
|
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");
|
const createdAt = record.get("createdAt");
|
||||||
|
|
||||||
// Create unique edge identifier to avoid duplicates
|
// 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 type { GoogleProfile } from "@coji/remix-auth-google";
|
||||||
import { prisma } from "~/db.server";
|
import { prisma } from "~/db.server";
|
||||||
import { env } from "~/env.server";
|
import { env } from "~/env.server";
|
||||||
import { ensureBillingInitialized } from "~/services/billing.server";
|
|
||||||
export type { User } from "@core/database";
|
export type { User } from "@core/database";
|
||||||
|
|
||||||
type FindOrCreateMagicLink = {
|
type FindOrCreateMagicLink = {
|
||||||
@ -167,7 +166,12 @@ export async function findOrCreateGoogleUser({
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function getUserById(id: User["id"]) {
|
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) {
|
if (!user) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@ -25,7 +25,7 @@ import {
|
|||||||
type ToastMessage,
|
type ToastMessage,
|
||||||
} from "./models/message.server";
|
} from "./models/message.server";
|
||||||
import { env } from "./env.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 { usePostHog } from "./hooks/usePostHog";
|
||||||
import {
|
import {
|
||||||
AppContainer,
|
AppContainer,
|
||||||
@ -40,6 +40,7 @@ import {
|
|||||||
useTheme,
|
useTheme,
|
||||||
} from "remix-themes";
|
} from "remix-themes";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
|
import { getUsageSummary } from "./services/billing.server";
|
||||||
|
|
||||||
export const links: LinksFunction = () => [{ rel: "stylesheet", href: styles }];
|
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 posthogProjectKey = env.POSTHOG_PROJECT_KEY;
|
||||||
const user = await getUser(request);
|
const user = await getUser(request);
|
||||||
const usage = await getUserRemainingCount(request);
|
const usageSummary = await getUsageSummary(user?.Workspace?.id as string);
|
||||||
|
|
||||||
return typedjson(
|
return typedjson(
|
||||||
{
|
{
|
||||||
user: user,
|
user: user,
|
||||||
availableCredits: usage?.availableCredits ?? 0,
|
availableCredits: usageSummary?.credits.available ?? 0,
|
||||||
|
totalCredits: usageSummary?.credits.monthly ?? 0,
|
||||||
toastMessage,
|
toastMessage,
|
||||||
theme: getTheme(),
|
theme: getTheme(),
|
||||||
posthogProjectKey,
|
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",
|
corsStrategy: "all",
|
||||||
allowJWT: true,
|
allowJWT: true,
|
||||||
},
|
},
|
||||||
async ({ params }) => {
|
async ({ params, authentication }) => {
|
||||||
const formattedLog = await getIngestionQueueForFrontend(params.logId);
|
const formattedLog = await getIngestionQueueForFrontend(
|
||||||
|
params.logId,
|
||||||
|
authentication.userId,
|
||||||
|
);
|
||||||
|
|
||||||
return json({ log: formattedLog });
|
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(),
|
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
|
// Search query schema
|
||||||
const SearchParamsSchema = z.object({
|
const SearchParamsSchema = z.object({
|
||||||
q: z.string().optional(),
|
q: z.string().optional(),
|
||||||
@ -36,7 +23,7 @@ const SearchParamsSchema = z.object({
|
|||||||
|
|
||||||
const { action } = createHybridActionApiRoute(
|
const { action } = createHybridActionApiRoute(
|
||||||
{
|
{
|
||||||
body: z.union([CreateSpaceSchema, BulkOperationSchema]),
|
body: CreateSpaceSchema,
|
||||||
allowJWT: true,
|
allowJWT: true,
|
||||||
authorization: {
|
authorization: {
|
||||||
action: "manage",
|
action: "manage",
|
||||||
@ -82,96 +69,6 @@ const { action } = createHybridActionApiRoute(
|
|||||||
return json({ space, success: true });
|
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 });
|
return json({ error: "Method not allowed" }, { status: 405 });
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@ -9,11 +9,11 @@ import { getIngestionQueueForFrontend } from "~/services/ingestionLogs.server";
|
|||||||
import { requireUserId } from "~/services/session.server";
|
import { requireUserId } from "~/services/session.server";
|
||||||
|
|
||||||
export async function loader({ request, params }: LoaderFunctionArgs) {
|
export async function loader({ request, params }: LoaderFunctionArgs) {
|
||||||
await requireUserId(request);
|
const userId = await requireUserId(request);
|
||||||
const logId = params.logId;
|
const logId = params.logId;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const log = await getIngestionQueueForFrontend(logId as string);
|
const log = await getIngestionQueueForFrontend(logId as string, userId);
|
||||||
return json({ log: log });
|
return json({ log: log });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return json({ log: null });
|
return json({ log: null });
|
||||||
|
|||||||
@ -3,11 +3,13 @@ import { type LoaderFunctionArgs } from "@remix-run/server-runtime";
|
|||||||
import { useLoaderData } from "@remix-run/react";
|
import { useLoaderData } from "@remix-run/react";
|
||||||
import { requireUserId } from "~/services/session.server";
|
import { requireUserId } from "~/services/session.server";
|
||||||
import { SpaceService } from "~/services/space.server";
|
import { SpaceService } from "~/services/space.server";
|
||||||
import { SpaceFactsFilters } from "~/components/spaces/space-facts-filters";
|
import { SpaceEpisodesFilters } from "~/components/spaces/space-episode-filters";
|
||||||
import { SpaceFactsList } from "~/components/spaces/space-facts-list";
|
import { SpaceEpisodesList } from "~/components/spaces/space-episodes-list";
|
||||||
|
|
||||||
import { ClientOnly } from "remix-utils/client-only";
|
import { ClientOnly } from "remix-utils/client-only";
|
||||||
import { LoaderCircle } from "lucide-react";
|
import { LoaderCircle } from "lucide-react";
|
||||||
|
import { getLogByEpisode } from "~/services/ingestionLogs.server";
|
||||||
|
import { Button } from "~/components/ui";
|
||||||
|
|
||||||
export async function loader({ request, params }: LoaderFunctionArgs) {
|
export async function loader({ request, params }: LoaderFunctionArgs) {
|
||||||
const userId = await requireUserId(request);
|
const userId = await requireUserId(request);
|
||||||
@ -15,16 +17,27 @@ export async function loader({ request, params }: LoaderFunctionArgs) {
|
|||||||
|
|
||||||
const spaceId = params.spaceId as string;
|
const spaceId = params.spaceId as string;
|
||||||
const space = await spaceService.getSpace(spaceId, userId);
|
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 {
|
return {
|
||||||
space,
|
space,
|
||||||
statements: statements || [],
|
episodes: episodesWithLogData || [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Facts() {
|
export default function Episodes() {
|
||||||
const { statements } = useLoaderData<typeof loader>();
|
const { episodes } = useLoaderData<typeof loader>();
|
||||||
const [selectedValidDate, setSelectedValidDate] = useState<
|
const [selectedValidDate, setSelectedValidDate] = useState<
|
||||||
string | undefined
|
string | undefined
|
||||||
>();
|
>();
|
||||||
@ -32,42 +45,27 @@ export default function Facts() {
|
|||||||
string | undefined
|
string | undefined
|
||||||
>();
|
>();
|
||||||
|
|
||||||
// Filter statements based on selected filters
|
// Filter episodes based on selected filters
|
||||||
const filteredStatements = statements.filter((statement) => {
|
const filteredEpisodes = episodes.filter((episode) => {
|
||||||
// Date filter
|
// Date filter
|
||||||
if (selectedValidDate) {
|
if (selectedValidDate) {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const statementDate = new Date(statement.validAt);
|
const episodeDate = new Date(episode.createdAt);
|
||||||
|
|
||||||
switch (selectedValidDate) {
|
switch (selectedValidDate) {
|
||||||
case "last_week":
|
case "last_week":
|
||||||
const weekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
|
const weekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
|
||||||
if (statementDate < weekAgo) return false;
|
if (episodeDate < weekAgo) return false;
|
||||||
break;
|
break;
|
||||||
case "last_month":
|
case "last_month":
|
||||||
const monthAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
|
const monthAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
|
||||||
if (statementDate < monthAgo) return false;
|
if (episodeDate < monthAgo) return false;
|
||||||
break;
|
break;
|
||||||
case "last_6_months":
|
case "last_6_months":
|
||||||
const sixMonthsAgo = new Date(
|
const sixMonthsAgo = new Date(
|
||||||
now.getTime() - 180 * 24 * 60 * 60 * 1000,
|
now.getTime() - 180 * 24 * 60 * 60 * 1000,
|
||||||
);
|
);
|
||||||
if (statementDate < sixMonthsAgo) return false;
|
if (episodeDate < 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:
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -81,20 +79,22 @@ export default function Facts() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full w-full flex-col pt-5">
|
<div className="flex h-full w-full flex-col pt-5">
|
||||||
<SpaceFactsFilters
|
<div className="mb-2 flex w-full items-center justify-start gap-2 px-5">
|
||||||
selectedValidDate={selectedValidDate}
|
<SpaceEpisodesFilters
|
||||||
selectedSpaceFilter={selectedSpaceFilter}
|
selectedValidDate={selectedValidDate}
|
||||||
onValidDateChange={setSelectedValidDate}
|
selectedSpaceFilter={selectedSpaceFilter}
|
||||||
onSpaceFilterChange={setSelectedSpaceFilter}
|
onValidDateChange={setSelectedValidDate}
|
||||||
/>
|
onSpaceFilterChange={setSelectedSpaceFilter}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex h-[calc(100vh_-_56px)] w-full">
|
<div className="flex h-[calc(100vh_-_56px)] w-full">
|
||||||
<ClientOnly
|
<ClientOnly
|
||||||
fallback={<LoaderCircle className="mr-2 h-4 w-4 animate-spin" />}
|
fallback={<LoaderCircle className="mr-2 h-4 w-4 animate-spin" />}
|
||||||
>
|
>
|
||||||
{() => (
|
{() => (
|
||||||
<SpaceFactsList
|
<SpaceEpisodesList
|
||||||
facts={filteredStatements}
|
episodes={filteredEpisodes}
|
||||||
hasMore={false} // TODO: Implement real pagination
|
hasMore={false} // TODO: Implement real pagination
|
||||||
loadMore={loadMore}
|
loadMore={loadMore}
|
||||||
isLoading={false}
|
isLoading={false}
|
||||||
@ -148,7 +148,7 @@ export default function Overview() {
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
className="text-muted-foreground mb-1 -ml-2 gap-1"
|
className="text-muted-foreground mb-1 -ml-2 gap-1"
|
||||||
>
|
>
|
||||||
Summary
|
Context
|
||||||
<ChevronDown
|
<ChevronDown
|
||||||
size={14}
|
size={14}
|
||||||
className={`transition-transform duration-300 ${
|
className={`transition-transform duration-300 ${
|
||||||
|
|||||||
@ -7,6 +7,9 @@ import { useTypedLoaderData } from "remix-typedjson";
|
|||||||
import { Outlet, useLocation, useNavigate } from "@remix-run/react";
|
import { Outlet, useLocation, useNavigate } from "@remix-run/react";
|
||||||
import { SpaceOptions } from "~/components/spaces/space-options";
|
import { SpaceOptions } from "~/components/spaces/space-options";
|
||||||
import { LoaderCircle } from "lucide-react";
|
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) {
|
export async function loader({ request, params }: LoaderFunctionArgs) {
|
||||||
const userId = await requireUserId(request);
|
const userId = await requireUserId(request);
|
||||||
@ -23,6 +26,7 @@ export default function Space() {
|
|||||||
const space = useTypedLoaderData<typeof loader>();
|
const space = useTypedLoaderData<typeof loader>();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const [showAddMemory, setShowAddMemory] = React.useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -46,16 +50,10 @@ export default function Space() {
|
|||||||
onClick: () => navigate(`/home/space/${space.id}/overview`),
|
onClick: () => navigate(`/home/space/${space.id}/overview`),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Facts",
|
label: "Episodes",
|
||||||
value: "facts",
|
value: "edpisodes",
|
||||||
isActive: location.pathname.includes("/facts"),
|
isActive: location.pathname.includes("/episodes"),
|
||||||
onClick: () => navigate(`/home/space/${space.id}/facts`),
|
onClick: () => navigate(`/home/space/${space.id}/episodes`),
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Patterns",
|
|
||||||
value: "patterns",
|
|
||||||
isActive: location.pathname.includes("/patterns"),
|
|
||||||
onClick: () => navigate(`/home/space/${space.id}/patterns`),
|
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
actionsNode={
|
actionsNode={
|
||||||
@ -67,17 +65,33 @@ export default function Space() {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
{() => (
|
{() => (
|
||||||
<SpaceOptions
|
<div className="flex items-center gap-2">
|
||||||
id={space.id as string}
|
<Button
|
||||||
name={space.name}
|
variant="secondary"
|
||||||
description={space.description}
|
onClick={() => setShowAddMemory(true)}
|
||||||
/>
|
>
|
||||||
|
Add episode
|
||||||
|
</Button>
|
||||||
|
<SpaceOptions
|
||||||
|
id={space.id as string}
|
||||||
|
name={space.name}
|
||||||
|
description={space.description}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</ClientOnly>
|
</ClientOnly>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<div className="relative flex h-[calc(100vh_-_56px)] w-full flex-col items-center justify-start overflow-auto">
|
<div className="relative flex h-[calc(100vh_-_56px)] w-full flex-col items-center justify-start overflow-auto">
|
||||||
<Outlet />
|
<Outlet />
|
||||||
|
|
||||||
|
{showAddMemory && (
|
||||||
|
<AddMemoryDialog
|
||||||
|
open={showAddMemory}
|
||||||
|
onOpenChange={setShowAddMemory}
|
||||||
|
defaultSpaceId={space.id}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -153,6 +153,7 @@ export default function Onboarding() {
|
|||||||
setCurrentQuestion(currentQuestion + 1);
|
setCurrentQuestion(currentQuestion + 1);
|
||||||
} else {
|
} else {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
// Submit all answers
|
// Submit all answers
|
||||||
submitAnswers();
|
submitAnswers();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -262,6 +262,7 @@ export default function BillingSettings() {
|
|||||||
<Progress
|
<Progress
|
||||||
segments={[{ value: 100 - usageSummary.credits.percentageUsed }]}
|
segments={[{ value: 100 - usageSummary.credits.percentageUsed }]}
|
||||||
className="mb-2"
|
className="mb-2"
|
||||||
|
color="#c15e50"
|
||||||
/>
|
/>
|
||||||
<p className="text-muted-foreground text-xs">
|
<p className="text-muted-foreground text-xs">
|
||||||
{usageSummary.credits.percentageUsed}% used this period
|
{usageSummary.credits.percentageUsed}% used this period
|
||||||
@ -452,7 +453,7 @@ export default function BillingSettings() {
|
|||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<div className="grid gap-6 p-6 md:grid-cols-3">
|
<div className="grid gap-6 md:grid-cols-3">
|
||||||
{/* Free Plan */}
|
{/* Free Plan */}
|
||||||
<Card className="p-6">
|
<Card className="p-6">
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
@ -467,10 +468,10 @@ export default function BillingSettings() {
|
|||||||
</div>
|
</div>
|
||||||
<ul className="mb-6 space-y-2 text-sm">
|
<ul className="mb-6 space-y-2 text-sm">
|
||||||
<li className="flex items-start gap-2">
|
<li className="flex items-start gap-2">
|
||||||
<span>Memory facts: 3k/mo</span>
|
<span>Credits: 3k/mo</span>
|
||||||
</li>
|
</li>
|
||||||
<li className="flex items-start gap-2">
|
<li className="flex items-start gap-2">
|
||||||
<span>NO USAGE BASED</span>
|
<span>No usage based</span>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<Button
|
<Button
|
||||||
@ -504,14 +505,15 @@ export default function BillingSettings() {
|
|||||||
</div>
|
</div>
|
||||||
<ul className="mb-6 space-y-2 text-sm">
|
<ul className="mb-6 space-y-2 text-sm">
|
||||||
<li className="flex items-start gap-2">
|
<li className="flex items-start gap-2">
|
||||||
<span>Memory facts: 15k/mo</span>
|
<span>Credits: 15k/mo</span>
|
||||||
</li>
|
</li>
|
||||||
<li className="flex items-start gap-2">
|
<li className="flex items-start gap-2">
|
||||||
<span>$0.299 /1K ADDITIONAL FACTS</span>
|
<span>$0.299 /1K Additional Credits</span>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<Button
|
<Button
|
||||||
className="w-full"
|
className="w-full"
|
||||||
|
variant="secondary"
|
||||||
disabled={
|
disabled={
|
||||||
usageSummary.plan.type === "PRO" ||
|
usageSummary.plan.type === "PRO" ||
|
||||||
fetcher.state === "submitting"
|
fetcher.state === "submitting"
|
||||||
@ -540,14 +542,15 @@ export default function BillingSettings() {
|
|||||||
</div>
|
</div>
|
||||||
<ul className="mb-6 space-y-2 text-sm">
|
<ul className="mb-6 space-y-2 text-sm">
|
||||||
<li className="flex items-start gap-2">
|
<li className="flex items-start gap-2">
|
||||||
<span>Memory facts: 100k/mo</span>
|
<span>Credits: 100k/mo</span>
|
||||||
</li>
|
</li>
|
||||||
<li className="flex items-start gap-2">
|
<li className="flex items-start gap-2">
|
||||||
<span>$0.249 /1K ADDITIONAL FACTS</span>
|
<span>$0.249 /1K Additional Credits</span>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<Button
|
<Button
|
||||||
className="w-full"
|
className="w-full"
|
||||||
|
variant="secondary"
|
||||||
disabled={
|
disabled={
|
||||||
usageSummary.plan.type === "MAX" ||
|
usageSummary.plan.type === "MAX" ||
|
||||||
fetcher.state === "submitting"
|
fetcher.state === "submitting"
|
||||||
|
|||||||
@ -160,6 +160,10 @@ export async function ensureBillingInitialized(workspaceId: string) {
|
|||||||
* Get workspace usage summary
|
* Get workspace usage summary
|
||||||
*/
|
*/
|
||||||
export async function getUsageSummary(workspaceId: string) {
|
export async function getUsageSummary(workspaceId: string) {
|
||||||
|
if (!workspaceId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
// Ensure billing records exist for existing accounts
|
// Ensure billing records exist for existing accounts
|
||||||
await ensureBillingInitialized(workspaceId);
|
await ensureBillingInitialized(workspaceId);
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,9 @@
|
|||||||
import { runQuery } from "~/lib/neo4j.server";
|
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> {
|
export async function saveEpisode(episode: EpisodicNode): Promise<string> {
|
||||||
const query = `
|
const query = `
|
||||||
@ -72,6 +76,8 @@ export async function getEpisode(uuid: string): Promise<EpisodicNode | null> {
|
|||||||
userId: episode.userId,
|
userId: episode.userId,
|
||||||
space: episode.space,
|
space: episode.space,
|
||||||
sessionId: episode.sessionId,
|
sessionId: episode.sessionId,
|
||||||
|
recallCount: episode.recallCount,
|
||||||
|
spaceIds: episode.spaceIds,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -140,7 +146,7 @@ export async function searchEpisodesByEmbedding(params: {
|
|||||||
}) {
|
}) {
|
||||||
const limit = params.limit || 100;
|
const limit = params.limit || 100;
|
||||||
const query = `
|
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
|
YIELD node AS episode
|
||||||
WHERE episode.userId = $userId
|
WHERE episode.userId = $userId
|
||||||
WITH episode, gds.similarity.cosine(episode.contentEmbedding, $embedding) AS score
|
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 limit = params.limit || 100;
|
||||||
const query = `
|
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
|
YIELD node AS episode
|
||||||
WHERE episode.userId = $userId
|
WHERE episode.userId = $userId
|
||||||
WITH episode, gds.similarity.cosine(episode.contentEmbedding, $embedding) AS score
|
WITH episode, gds.similarity.cosine(episode.contentEmbedding, $embedding) AS score
|
||||||
|
|||||||
@ -157,8 +157,12 @@ export async function deleteSpace(
|
|||||||
RETURN count(s) as updatedStatements
|
RETURN count(s) as updatedStatements
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const cleanupStatementsResult = await runQuery(cleanupStatementsQuery, { userId, spaceId });
|
const cleanupStatementsResult = await runQuery(cleanupStatementsQuery, {
|
||||||
const updatedStatements = cleanupStatementsResult[0]?.get("updatedStatements") || 0;
|
userId,
|
||||||
|
spaceId,
|
||||||
|
});
|
||||||
|
const updatedStatements =
|
||||||
|
cleanupStatementsResult[0]?.get("updatedStatements") || 0;
|
||||||
|
|
||||||
// 3. Clean up episode references (remove spaceId from spaceIds arrays)
|
// 3. Clean up episode references (remove spaceId from spaceIds arrays)
|
||||||
const cleanupEpisodesQuery = `
|
const cleanupEpisodesQuery = `
|
||||||
@ -168,8 +172,12 @@ export async function deleteSpace(
|
|||||||
RETURN count(e) as updatedEpisodes
|
RETURN count(e) as updatedEpisodes
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const cleanupEpisodesResult = await runQuery(cleanupEpisodesQuery, { userId, spaceId });
|
const cleanupEpisodesResult = await runQuery(cleanupEpisodesQuery, {
|
||||||
const updatedEpisodes = cleanupEpisodesResult[0]?.get("updatedEpisodes") || 0;
|
userId,
|
||||||
|
spaceId,
|
||||||
|
});
|
||||||
|
const updatedEpisodes =
|
||||||
|
cleanupEpisodesResult[0]?.get("updatedEpisodes") || 0;
|
||||||
|
|
||||||
// 4. Delete the space node and all its relationships
|
// 4. Delete the space node and all its relationships
|
||||||
const deleteQuery = `
|
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
|
* Assign episodes to a space using intent-based matching
|
||||||
*/
|
*/
|
||||||
@ -508,3 +356,36 @@ export async function getSpaceEpisodeCount(
|
|||||||
const result = await runQuery(query, { spaceId, userId });
|
const result = await runQuery(query, { spaceId, userId });
|
||||||
return Number(result[0]?.get("episodeCount") || 0);
|
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 { prisma } from "~/db.server";
|
||||||
|
import { getEpisode } from "./graphModels/episode";
|
||||||
|
import { getSpacesForEpisodes } from "./graphModels/space";
|
||||||
|
|
||||||
export async function getIngestionLogs(
|
export async function getIngestionLogs(
|
||||||
userId: string,
|
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
|
// Fetch the specific log by logId
|
||||||
const log = await prisma.ingestionQueue.findUnique({
|
const log = await prisma.ingestionQueue.findUnique({
|
||||||
where: { id: id },
|
where: { id: id },
|
||||||
@ -66,6 +71,7 @@ export const getIngestionQueueForFrontend = async (id: string) => {
|
|||||||
type: true,
|
type: true,
|
||||||
output: true,
|
output: true,
|
||||||
data: true,
|
data: true,
|
||||||
|
workspaceId: true,
|
||||||
activity: {
|
activity: {
|
||||||
select: {
|
select: {
|
||||||
text: true,
|
text: true,
|
||||||
@ -94,7 +100,7 @@ export const getIngestionQueueForFrontend = async (id: string) => {
|
|||||||
log.activity?.integrationAccount?.integrationDefinition;
|
log.activity?.integrationAccount?.integrationDefinition;
|
||||||
const logData = log.data as any;
|
const logData = log.data as any;
|
||||||
|
|
||||||
const formattedLog = {
|
const formattedLog: any = {
|
||||||
id: log.id,
|
id: log.id,
|
||||||
source: integrationDef?.name || logData?.source || "Unknown",
|
source: integrationDef?.name || logData?.source || "Unknown",
|
||||||
ingestText:
|
ingestText:
|
||||||
@ -112,9 +118,76 @@ export const getIngestionQueueForFrontend = async (id: string) => {
|
|||||||
data: log.data,
|
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;
|
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) => {
|
export const deleteIngestionQueue = async (id: string) => {
|
||||||
return await prisma.ingestionQueue.delete({
|
return await prisma.ingestionQueue.delete({
|
||||||
where: {
|
where: {
|
||||||
|
|||||||
@ -33,14 +33,6 @@ export async function getUser(request: Request) {
|
|||||||
throw await logout(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) {
|
export async function requireUserId(request: Request, redirectTo?: string) {
|
||||||
const userId = await getUserId(request);
|
const userId = await getUserId(request);
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
@ -71,6 +63,7 @@ export async function requireUser(request: Request) {
|
|||||||
confirmedBasicDetails: user.confirmedBasicDetails,
|
confirmedBasicDetails: user.confirmedBasicDetails,
|
||||||
onboardingComplete: user.onboardingComplete,
|
onboardingComplete: user.onboardingComplete,
|
||||||
isImpersonating: !!impersonationId,
|
isImpersonating: !!impersonationId,
|
||||||
|
workspaceId: user.Workspace?.id,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -3,20 +3,18 @@ import {
|
|||||||
type SpaceNode,
|
type SpaceNode,
|
||||||
type CreateSpaceParams,
|
type CreateSpaceParams,
|
||||||
type UpdateSpaceParams,
|
type UpdateSpaceParams,
|
||||||
type SpaceAssignmentResult,
|
|
||||||
} from "@core/types";
|
} from "@core/types";
|
||||||
import { type Space } from "@prisma/client";
|
import { type Space } from "@prisma/client";
|
||||||
|
|
||||||
import { triggerSpaceAssignment } from "~/trigger/spaces/space-assignment";
|
import { triggerSpaceAssignment } from "~/trigger/spaces/space-assignment";
|
||||||
import {
|
import {
|
||||||
assignEpisodesToSpace,
|
assignEpisodesToSpace,
|
||||||
assignStatementsToSpace,
|
|
||||||
createSpace,
|
createSpace,
|
||||||
deleteSpace,
|
deleteSpace,
|
||||||
getSpace,
|
getSpace,
|
||||||
getSpaceStatements,
|
getSpaceEpisodeCount,
|
||||||
initializeStatementSpaceIds,
|
getSpaceEpisodes,
|
||||||
removeStatementsFromSpace,
|
removeEpisodesFromSpace,
|
||||||
updateSpace,
|
updateSpace,
|
||||||
} from "./graphModels/space";
|
} from "./graphModels/space";
|
||||||
import { prisma } from "~/trigger/utils/prisma";
|
import { prisma } from "~/trigger/utils/prisma";
|
||||||
@ -278,97 +276,11 @@ export class SpaceService {
|
|||||||
return resetSpace;
|
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
|
* Get all episodes in a space
|
||||||
*/
|
*/
|
||||||
async getSpaceEpisodes(spaceId: string, userId: string) {
|
async getSpaceEpisodes(spaceId: string, userId: string) {
|
||||||
logger.info(`Fetching episodes for space ${spaceId} for user ${userId}`);
|
logger.info(`Fetching episodes for space ${spaceId} for user ${userId}`);
|
||||||
const { getSpaceEpisodes } = await import("./graphModels/space");
|
|
||||||
return await getSpaceEpisodes(spaceId, userId);
|
return await getSpaceEpisodes(spaceId, userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -384,7 +296,7 @@ export class SpaceService {
|
|||||||
`Assigning ${episodeIds.length} episodes to space ${spaceId} for user ${userId}`,
|
`Assigning ${episodeIds.length} episodes to space ${spaceId} for user ${userId}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
await assignEpisodesToSpace(episodeIds,spaceId, userId);
|
await assignEpisodesToSpace(episodeIds, spaceId, userId);
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
`Successfully assigned ${episodeIds.length} episodes to space ${spaceId}`,
|
`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}`,
|
`Removing ${episodeIds.length} episodes from space ${spaceId} for user ${userId}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
await this.removeEpisodesFromSpace(episodeIds, spaceId, userId);
|
await removeEpisodesFromSpace(episodeIds, spaceId, userId);
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
`Successfully removed ${episodeIds.length} episodes from space ${spaceId}`,
|
`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
|
* Validate space access
|
||||||
*/
|
*/
|
||||||
@ -482,41 +351,4 @@ export class SpaceService {
|
|||||||
const space = await this.getSpace(spaceId, userId);
|
const space = await this.getSpace(spaceId, userId);
|
||||||
return space !== null && space.isActive;
|
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-dom": "^18.2.0",
|
||||||
"react-markdown": "10.1.0",
|
"react-markdown": "10.1.0",
|
||||||
"react-resizable-panels": "^1.0.9",
|
"react-resizable-panels": "^1.0.9",
|
||||||
|
"react-hotkeys-hook": "^4.5.0",
|
||||||
"react-virtualized": "^9.22.6",
|
"react-virtualized": "^9.22.6",
|
||||||
"remix-auth": "^4.2.0",
|
"remix-auth": "^4.2.0",
|
||||||
"remix-auth-oauth2": "^3.4.1",
|
"remix-auth-oauth2": "^3.4.1",
|
||||||
@ -135,6 +136,7 @@
|
|||||||
"stripe": "19.0.0",
|
"stripe": "19.0.0",
|
||||||
"simple-oauth2": "^5.1.0",
|
"simple-oauth2": "^5.1.0",
|
||||||
"tailwind-merge": "^2.6.0",
|
"tailwind-merge": "^2.6.0",
|
||||||
|
"tiptap-markdown": "0.9.0",
|
||||||
"tailwind-scrollbar-hide": "^2.0.0",
|
"tailwind-scrollbar-hide": "^2.0.0",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"tailwindcss-textshadow": "^2.1.3",
|
"tailwindcss-textshadow": "^2.1.3",
|
||||||
|
|||||||
@ -39,6 +39,7 @@ export interface EpisodicNode {
|
|||||||
sessionId?: string;
|
sessionId?: string;
|
||||||
recallCount?: number;
|
recallCount?: number;
|
||||||
chunkIndex?: number; // Index of this chunk within the document
|
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:
|
react-dom:
|
||||||
specifier: ^18.2.0
|
specifier: ^18.2.0
|
||||||
version: 18.3.1(react@18.3.1)
|
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:
|
react-markdown:
|
||||||
specifier: 10.1.0
|
specifier: 10.1.0
|
||||||
version: 10.1.0(@types/react@18.2.69)(react@18.3.1)
|
version: 10.1.0(@types/react@18.2.69)(react@18.3.1)
|
||||||
@ -655,6 +658,9 @@ importers:
|
|||||||
tiny-invariant:
|
tiny-invariant:
|
||||||
specifier: ^1.3.1
|
specifier: ^1.3.1
|
||||||
version: 1.3.3
|
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:
|
zod:
|
||||||
specifier: 3.25.76
|
specifier: 3.25.76
|
||||||
version: 3.25.76
|
version: 3.25.76
|
||||||
@ -5741,9 +5747,15 @@ packages:
|
|||||||
'@types/json5@0.0.29':
|
'@types/json5@0.0.29':
|
||||||
resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==}
|
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':
|
'@types/linkify-it@5.0.0':
|
||||||
resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==}
|
resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==}
|
||||||
|
|
||||||
|
'@types/markdown-it@13.0.9':
|
||||||
|
resolution: {integrity: sha512-1XPwR0+MgXLWfTn9gCsZ55AHOKW1WN+P9vr0PaQh5aerR9LLQXUbjfEAFhjmEmyoYFWAyuN2Mqkn40MZ4ukjBw==}
|
||||||
|
|
||||||
'@types/markdown-it@14.1.2':
|
'@types/markdown-it@14.1.2':
|
||||||
resolution: {integrity: sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==}
|
resolution: {integrity: sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==}
|
||||||
|
|
||||||
@ -5753,6 +5765,9 @@ packages:
|
|||||||
'@types/mdast@4.0.4':
|
'@types/mdast@4.0.4':
|
||||||
resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==}
|
resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==}
|
||||||
|
|
||||||
|
'@types/mdurl@1.0.5':
|
||||||
|
resolution: {integrity: sha512-6L6VymKTzYSrEf4Nev4Xa1LCHKrlTlYCBMTlQKFuddo1CvQcE52I0mwfOJayueUC7MJuXOeHTcIU683lzd0cUA==}
|
||||||
|
|
||||||
'@types/mdurl@2.0.0':
|
'@types/mdurl@2.0.0':
|
||||||
resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==}
|
resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==}
|
||||||
|
|
||||||
@ -9066,6 +9081,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-WWC0ZuMzCyDHYCasEGs4IPvLyTGftYwh6wIEOULOF0HXcqZlhwRzrK0w2VUlxWA98xnvb/jszw4ZSkJ6ADpM6Q==}
|
resolution: {integrity: sha512-WWC0ZuMzCyDHYCasEGs4IPvLyTGftYwh6wIEOULOF0HXcqZlhwRzrK0w2VUlxWA98xnvb/jszw4ZSkJ6ADpM6Q==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
markdown-it-task-lists@2.1.1:
|
||||||
|
resolution: {integrity: sha512-TxFAc76Jnhb2OUu+n3yz9RMu4CwGfaT788br6HhEDlvWfdeJcLUsxk1Hgw2yJio0OXsxv7pyIPmvECY7bMbluA==}
|
||||||
|
|
||||||
markdown-it@14.1.0:
|
markdown-it@14.1.0:
|
||||||
resolution: {integrity: sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==}
|
resolution: {integrity: sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
@ -10584,6 +10602,12 @@ packages:
|
|||||||
engines: {node: '>=18.0.0'}
|
engines: {node: '>=18.0.0'}
|
||||||
hasBin: true
|
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:
|
react-is@16.13.1:
|
||||||
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
|
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
|
||||||
|
|
||||||
@ -11556,6 +11580,11 @@ packages:
|
|||||||
tiptap-extension-global-drag-handle@0.1.18:
|
tiptap-extension-global-drag-handle@0.1.18:
|
||||||
resolution: {integrity: sha512-jwFuy1K8DP3a4bFy76Hpc63w1Sil0B7uZ3mvhQomVvUFCU787Lg2FowNhn7NFzeyok761qY2VG+PZ/FDthWUdg==}
|
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:
|
tmp@0.0.33:
|
||||||
resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==}
|
resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==}
|
||||||
engines: {node: '>=0.6.0'}
|
engines: {node: '>=0.6.0'}
|
||||||
@ -18728,8 +18757,15 @@ snapshots:
|
|||||||
|
|
||||||
'@types/json5@0.0.29': {}
|
'@types/json5@0.0.29': {}
|
||||||
|
|
||||||
|
'@types/linkify-it@3.0.5': {}
|
||||||
|
|
||||||
'@types/linkify-it@5.0.0': {}
|
'@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':
|
'@types/markdown-it@14.1.2':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/linkify-it': 5.0.0
|
'@types/linkify-it': 5.0.0
|
||||||
@ -18743,6 +18779,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@types/unist': 3.0.3
|
'@types/unist': 3.0.3
|
||||||
|
|
||||||
|
'@types/mdurl@1.0.5': {}
|
||||||
|
|
||||||
'@types/mdurl@2.0.0': {}
|
'@types/mdurl@2.0.0': {}
|
||||||
|
|
||||||
'@types/mdx@2.0.13': {}
|
'@types/mdx@2.0.13': {}
|
||||||
@ -22608,6 +22646,8 @@ snapshots:
|
|||||||
|
|
||||||
markdown-extensions@1.1.1: {}
|
markdown-extensions@1.1.1: {}
|
||||||
|
|
||||||
|
markdown-it-task-lists@2.1.1: {}
|
||||||
|
|
||||||
markdown-it@14.1.0:
|
markdown-it@14.1.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
argparse: 2.0.1
|
argparse: 2.0.1
|
||||||
@ -24536,6 +24576,11 @@ snapshots:
|
|||||||
- utf-8-validate
|
- utf-8-validate
|
||||||
- webpack-cli
|
- 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@16.13.1: {}
|
||||||
|
|
||||||
react-is@17.0.2: {}
|
react-is@17.0.2: {}
|
||||||
@ -25790,6 +25835,14 @@ snapshots:
|
|||||||
|
|
||||||
tiptap-extension-global-drag-handle@0.1.18: {}
|
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:
|
tmp@0.0.33:
|
||||||
dependencies:
|
dependencies:
|
||||||
os-tmpdir: 1.0.2
|
os-tmpdir: 1.0.2
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user