mirror of
https://github.com/eliasstepanik/core.git
synced 2026-01-10 08:48:29 +00:00
fix: UI for document logs
feat: added logs API to delete the episode
This commit is contained in:
parent
43c3482351
commit
840ca64174
@ -57,9 +57,7 @@ export const ConversationList = ({
|
||||
limit: "5", // Increased for better density
|
||||
});
|
||||
|
||||
fetcher.load(`/api/v1/conversations?${searchParams}`, {
|
||||
flushSync: true,
|
||||
});
|
||||
fetcher.load(`/api/v1/conversations?${searchParams}`);
|
||||
},
|
||||
[isLoading, fetcher],
|
||||
);
|
||||
|
||||
@ -114,36 +114,6 @@ export const GraphVisualization = forwardRef<GraphRef, GraphVisualizationProps>(
|
||||
return (
|
||||
<div className={className}>
|
||||
{/* Entity Types Legend Button */}
|
||||
<div className="absolute top-4 left-4 z-50">
|
||||
{/* <HoverCard>
|
||||
<HoverCardTrigger asChild>
|
||||
<button className="bg-primary/10 text-primary hover:bg-primary/20 rounded-md px-2.5 py-1 text-xs transition-colors">
|
||||
Entity Types
|
||||
</button>
|
||||
</HoverCardTrigger>
|
||||
<HoverCardContent className="w-40" side="bottom" align="start">
|
||||
<div className="space-y-2">
|
||||
<div className="max-h-[300px] space-y-1.5 overflow-y-auto pr-2">
|
||||
{allLabels.map((label) => (
|
||||
<div key={label} className="flex items-center gap-2">
|
||||
<div
|
||||
className="h-4 w-4 flex-shrink-0 rounded-full"
|
||||
style={{
|
||||
backgroundColor: getNodeColor(
|
||||
label,
|
||||
isDarkMode,
|
||||
sharedLabelColorMap,
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<span className="text-xs">{label}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</HoverCardContent>
|
||||
</HoverCard> */}
|
||||
</div>
|
||||
|
||||
{triplets.length > 0 ? (
|
||||
<Graph
|
||||
|
||||
@ -1,13 +1,13 @@
|
||||
import { useState, useEffect, type ReactNode } from "react";
|
||||
import { useFetcher } from "@remix-run/react";
|
||||
import { AlertCircle, Loader2 } from "lucide-react";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "../ui/dialog";
|
||||
import { Badge, BadgeColor } from "../ui/badge";
|
||||
import { type LogItem } from "~/hooks/use-logs";
|
||||
import Markdown from "react-markdown";
|
||||
import { getIconForAuthorise } from "../icon-utils";
|
||||
import { cn } from "~/lib/utils";
|
||||
import { cn, formatString } from "~/lib/utils";
|
||||
import { getStatusColor } from "./utils";
|
||||
import { format } from "date-fns";
|
||||
|
||||
interface LogDetailsProps {
|
||||
log: LogItem;
|
||||
@ -46,9 +46,7 @@ function PropertyItem({
|
||||
{statusColor && (
|
||||
<BadgeColor className={cn(statusColor, "h-2.5 w-2.5")} />
|
||||
)}
|
||||
{typeof value === "string"
|
||||
? value.charAt(0).toUpperCase() + value.slice(1).toLowerCase()
|
||||
: value}
|
||||
{value}
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge variant={variant} className={cn("h-7 rounded px-4", className)}>
|
||||
@ -73,6 +71,14 @@ interface EpisodeFactsResponse {
|
||||
invalidFacts: EpisodeFact[];
|
||||
}
|
||||
|
||||
function getStatusValue(status: string) {
|
||||
if (status === "PENDING") {
|
||||
return "In Queue";
|
||||
}
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
export function LogDetails({ log }: LogDetailsProps) {
|
||||
const [facts, setFacts] = useState<any[]>([]);
|
||||
const [invalidFacts, setInvalidFacts] = useState<any[]>([]);
|
||||
@ -122,8 +128,8 @@ export function LogDetails({ log }: LogDetailsProps) {
|
||||
}, [fetcher.data, fetcher.state]);
|
||||
|
||||
return (
|
||||
<div className="flex w-full flex-col items-center">
|
||||
<div className="w-4xl">
|
||||
<div className="flex h-full w-full flex-col items-center overflow-auto">
|
||||
<div className="max-w-4xl">
|
||||
<div className="px-4 pt-4">
|
||||
<div className="mb-4 flex w-full items-center justify-between">
|
||||
<span>Episode Details</span>
|
||||
@ -131,7 +137,7 @@ export function LogDetails({ log }: LogDetailsProps) {
|
||||
</div>
|
||||
|
||||
<div className="mb-10 px-4">
|
||||
<div className="space-y-3">
|
||||
<div className="space-y-1">
|
||||
{log.data?.type === "DOCUMENT" && log.data?.episodes ? (
|
||||
<PropertyItem
|
||||
label="Episodes"
|
||||
@ -166,14 +172,14 @@ export function LogDetails({ log }: LogDetailsProps) {
|
||||
/>
|
||||
<PropertyItem
|
||||
label="Type"
|
||||
value={
|
||||
log.data?.type ? log.data.type.toLowerCase() : "conversation"
|
||||
}
|
||||
value={formatString(
|
||||
log.data?.type ? log.data.type.toLowerCase() : "conversation",
|
||||
)}
|
||||
variant="secondary"
|
||||
/>
|
||||
<PropertyItem
|
||||
label="Source"
|
||||
value={log.source?.toLowerCase()}
|
||||
value={formatString(log.source?.toLowerCase())}
|
||||
icon={
|
||||
log.source &&
|
||||
getIconForAuthorise(log.source.toLowerCase(), 16, undefined)
|
||||
@ -183,7 +189,7 @@ export function LogDetails({ log }: LogDetailsProps) {
|
||||
|
||||
<PropertyItem
|
||||
label="Status"
|
||||
value={log.status}
|
||||
value={getStatusValue(log.status)}
|
||||
variant="status"
|
||||
statusColor={log.status && getStatusColor(log.status)}
|
||||
/>
|
||||
@ -207,6 +213,18 @@ export function LogDetails({ log }: LogDetailsProps) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col items-center p-4 pt-0">
|
||||
<div className="mb-2 flex w-full items-center justify-between">
|
||||
<span>Content</span>
|
||||
</div>
|
||||
{/* Log Content */}
|
||||
<div className="mb-4 text-sm break-words whitespace-pre-wrap">
|
||||
<div className="rounded-md">
|
||||
<Markdown>{log.ingestText}</Markdown>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Episode Facts */}
|
||||
<div className="mb-6 px-4">
|
||||
<div className="mb-2 flex w-full items-center justify-between">
|
||||
@ -218,20 +236,21 @@ export function LogDetails({ log }: LogDetailsProps) {
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
</div>
|
||||
) : facts.length > 0 ? (
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex flex-col gap-1">
|
||||
{facts.map((fact) => (
|
||||
<div
|
||||
key={fact.uuid}
|
||||
className="bg-grayAlpha-100 rounded-md p-3"
|
||||
className="bg-grayAlpha-100 flex items-center justify-between gap-2 rounded-md p-3"
|
||||
>
|
||||
<p className="mb-1 text-sm">{fact.fact}</p>
|
||||
<div className="text-muted-foreground flex items-center gap-2 text-xs">
|
||||
<p className="text-sm">{fact.fact}</p>
|
||||
<div className="text-muted-foreground flex shrink-0 items-center gap-2 text-xs">
|
||||
<span>
|
||||
Valid: {new Date(fact.validAt).toLocaleString()}
|
||||
Valid: {format(new Date(fact.validAt), "dd/MM/yyyy")}
|
||||
</span>
|
||||
{fact.invalidAt && (
|
||||
<span>
|
||||
Invalid: {new Date(fact.invalidAt).toLocaleString()}
|
||||
Invalid:{" "}
|
||||
{format(new Date(fact.invalidAt), "dd/MM/yyyy")}
|
||||
</span>
|
||||
)}
|
||||
{Object.keys(fact.attributes).length > 0 && (
|
||||
@ -270,15 +289,6 @@ export function LogDetails({ log }: LogDetailsProps) {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex max-h-[88vh] flex-col items-center overflow-auto p-4 pt-0">
|
||||
{/* Log Content */}
|
||||
<div className="mb-4 text-sm break-words whitespace-pre-wrap">
|
||||
<div className="rounded-md">
|
||||
<Markdown>{log.ingestText}</Markdown>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -17,7 +17,7 @@ import {
|
||||
AlertDialogTitle,
|
||||
} from "../ui/alert-dialog";
|
||||
import { useState, useEffect } from "react";
|
||||
import { redirect, useFetcher } from "@remix-run/react";
|
||||
import { useFetcher, useNavigate } from "@remix-run/react";
|
||||
|
||||
interface LogOptionsProps {
|
||||
id: string;
|
||||
@ -26,6 +26,7 @@ interface LogOptionsProps {
|
||||
export const LogOptions = ({ id }: LogOptionsProps) => {
|
||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||
const deleteFetcher = useFetcher<{ success: boolean }>();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleDelete = () => {
|
||||
deleteFetcher.submit(
|
||||
@ -40,8 +41,9 @@ export const LogOptions = ({ id }: LogOptionsProps) => {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
console.log(deleteFetcher.state, deleteFetcher.data);
|
||||
if (deleteFetcher.state === "idle" && deleteFetcher.data?.success) {
|
||||
redirect(`/home/inbox`);
|
||||
navigate(`/home/inbox`);
|
||||
}
|
||||
}, [deleteFetcher.state, deleteFetcher.data]);
|
||||
|
||||
|
||||
@ -3,7 +3,7 @@ import { Badge, BadgeColor } from "../ui/badge";
|
||||
import { type LogItem } from "~/hooks/use-logs";
|
||||
import { getIconForAuthorise } from "../icon-utils";
|
||||
import { useNavigate, useParams } from "@remix-run/react";
|
||||
import { getStatusColor } from "./utils";
|
||||
import { getStatusColor, getStatusValue } from "./utils";
|
||||
|
||||
interface LogTextCollapseProps {
|
||||
text?: string;
|
||||
@ -83,8 +83,7 @@ export function LogTextCollapse({ text, log }: LogTextCollapseProps) {
|
||||
)}
|
||||
>
|
||||
<BadgeColor className={cn(getStatusColor(log.status))} />
|
||||
{log.status.charAt(0).toUpperCase() +
|
||||
log.status.slice(1).toLowerCase()}
|
||||
{getStatusValue(log.status)}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import { formatString } from "~/lib/utils";
|
||||
|
||||
export const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case "PROCESSING":
|
||||
@ -14,3 +16,11 @@ export const getStatusColor = (status: string) => {
|
||||
return "bg-gray-800";
|
||||
}
|
||||
};
|
||||
|
||||
export function getStatusValue(status: string) {
|
||||
if (status === "PENDING") {
|
||||
return formatString("In Queue");
|
||||
}
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
@ -86,6 +86,7 @@ const EnvironmentSchema = z.object({
|
||||
// Model envs
|
||||
MODEL: z.string().default(LLMModelEnum.GPT41),
|
||||
EMBEDDING_MODEL: z.string().default("mxbai-embed-large"),
|
||||
EMBEDDING_MODEL_SIZE: z.string().default("1024"),
|
||||
OLLAMA_URL: z.string().optional(),
|
||||
COHERE_API_KEY: z.string().optional(),
|
||||
});
|
||||
|
||||
@ -5,6 +5,7 @@ import { singleton } from "~/utils/singleton";
|
||||
|
||||
// Create a singleton driver instance
|
||||
const driver = singleton("neo4j", getDriver);
|
||||
const EMBEDDING_MODEL_SIZE = process.env.EMBEDDING_MODEL_SIZE ?? "1024";
|
||||
|
||||
function getDriver() {
|
||||
return neo4j.driver(
|
||||
@ -373,17 +374,17 @@ const initializeSchema = async () => {
|
||||
// Create vector indexes for semantic search (if using Neo4j 5.0+)
|
||||
await runQuery(`
|
||||
CREATE VECTOR INDEX entity_embedding IF NOT EXISTS FOR (n:Entity) ON n.nameEmbedding
|
||||
OPTIONS {indexConfig: {\`vector.dimensions\`: 1024, \`vector.similarity_function\`: 'cosine', \`vector.hnsw.ef_construction\`: 400, \`vector.hnsw.m\`: 32}}
|
||||
OPTIONS {indexConfig: {\`vector.dimensions\`: ${EMBEDDING_MODEL_SIZE}, \`vector.similarity_function\`: 'cosine', \`vector.hnsw.ef_construction\`: 400, \`vector.hnsw.m\`: 32}}
|
||||
`);
|
||||
|
||||
await runQuery(`
|
||||
CREATE VECTOR INDEX statement_embedding IF NOT EXISTS FOR (n:Statement) ON n.factEmbedding
|
||||
OPTIONS {indexConfig: {\`vector.dimensions\`: 1024, \`vector.similarity_function\`: 'cosine', \`vector.hnsw.ef_construction\`: 400, \`vector.hnsw.m\`: 32}}
|
||||
OPTIONS {indexConfig: {\`vector.dimensions\`: ${EMBEDDING_MODEL_SIZE}, \`vector.similarity_function\`: 'cosine', \`vector.hnsw.ef_construction\`: 400, \`vector.hnsw.m\`: 32}}
|
||||
`);
|
||||
|
||||
await runQuery(`
|
||||
CREATE VECTOR INDEX episode_embedding IF NOT EXISTS FOR (n:Episode) ON n.contentEmbedding
|
||||
OPTIONS {indexConfig: {\`vector.dimensions\`: 1024, \`vector.similarity_function\`: 'cosine', \`vector.hnsw.ef_construction\`: 400, \`vector.hnsw.m\`: 32}}
|
||||
OPTIONS {indexConfig: {\`vector.dimensions\`: ${EMBEDDING_MODEL_SIZE}, \`vector.similarity_function\`: 'cosine', \`vector.hnsw.ef_construction\`: 400, \`vector.hnsw.m\`: 32}}
|
||||
`);
|
||||
|
||||
// Create fulltext indexes for BM25 search
|
||||
|
||||
@ -1,6 +1,14 @@
|
||||
import { clsx, type ClassValue } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
import { clsx, type ClassValue } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
|
||||
export function formatString(input: string): string {
|
||||
if (!input) return "";
|
||||
return input
|
||||
.split(" ")
|
||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
||||
.join(" ");
|
||||
}
|
||||
|
||||
@ -6,6 +6,7 @@ import {
|
||||
deleteIngestionQueue,
|
||||
getIngestionQueue,
|
||||
} from "~/services/ingestionLogs.server";
|
||||
import { runs, tasks } from "@trigger.dev/sdk";
|
||||
|
||||
export const DeleteEpisodeBodyRequest = z.object({
|
||||
id: z.string(),
|
||||
@ -36,6 +37,21 @@ const { action, loader } = createHybridActionApiRoute(
|
||||
}
|
||||
|
||||
const output = ingestionQueue.output as any;
|
||||
const runningTasks = await runs.list({
|
||||
tag: [authentication.userId, ingestionQueue.id],
|
||||
taskIdentifier: "ingest-episode",
|
||||
});
|
||||
|
||||
const latestTask = runningTasks.data.find(
|
||||
(task) =>
|
||||
task.tags.includes(authentication.userId) &&
|
||||
task.tags.includes(ingestionQueue.id),
|
||||
);
|
||||
|
||||
if (latestTask && !latestTask?.isCompleted) {
|
||||
runs.cancel(latestTask?.id as string);
|
||||
}
|
||||
|
||||
let result;
|
||||
|
||||
if (output?.episodeUuid) {
|
||||
|
||||
@ -1,78 +1,122 @@
|
||||
import { type LoaderFunctionArgs, json } from "@remix-run/node";
|
||||
import { prisma } from "~/db.server";
|
||||
import { requireUserId } from "~/services/session.server";
|
||||
import { json } from "@remix-run/node";
|
||||
import { runs } from "@trigger.dev/sdk";
|
||||
import { z } from "zod";
|
||||
import { deleteEpisodeWithRelatedNodes } from "~/services/graphModels/episode";
|
||||
import {
|
||||
deleteIngestionQueue,
|
||||
getIngestionQueue,
|
||||
getIngestionQueueForFrontend,
|
||||
} from "~/services/ingestionLogs.server";
|
||||
import {
|
||||
createHybridActionApiRoute,
|
||||
createHybridLoaderApiRoute,
|
||||
} from "~/services/routeBuilders/apiBuilder.server";
|
||||
|
||||
export async function loader({ request, params }: LoaderFunctionArgs) {
|
||||
const userId = await requireUserId(request);
|
||||
const logId = params.logId;
|
||||
// Schema for space ID parameter
|
||||
const LogParamsSchema = z.object({
|
||||
logId: z.string(),
|
||||
});
|
||||
|
||||
// Get user and workspace in one query
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
select: { Workspace: { select: { id: true } } },
|
||||
});
|
||||
const loader = createHybridLoaderApiRoute(
|
||||
{
|
||||
params: LogParamsSchema,
|
||||
findResource: async () => 1,
|
||||
corsStrategy: "all",
|
||||
allowJWT: true,
|
||||
},
|
||||
async ({ params, authentication }) => {
|
||||
const formattedLog = await getIngestionQueueForFrontend(params.logId);
|
||||
|
||||
if (!user?.Workspace) {
|
||||
throw new Response("Workspace not found", { status: 404 });
|
||||
}
|
||||
return json({ log: formattedLog });
|
||||
},
|
||||
);
|
||||
|
||||
// Fetch the specific log by logId
|
||||
const log = await prisma.ingestionQueue.findUnique({
|
||||
where: { id: logId },
|
||||
select: {
|
||||
id: true,
|
||||
createdAt: true,
|
||||
processedAt: true,
|
||||
status: true,
|
||||
error: true,
|
||||
type: true,
|
||||
output: true,
|
||||
data: true,
|
||||
activity: {
|
||||
select: {
|
||||
text: true,
|
||||
sourceURL: true,
|
||||
integrationAccount: {
|
||||
select: {
|
||||
integrationDefinition: {
|
||||
select: {
|
||||
name: true,
|
||||
slug: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
export const DeleteEpisodeBodyRequest = z.object({
|
||||
id: z.string(),
|
||||
});
|
||||
|
||||
const { action } = createHybridActionApiRoute(
|
||||
{
|
||||
body: DeleteEpisodeBodyRequest,
|
||||
allowJWT: true,
|
||||
method: "DELETE",
|
||||
authorization: {
|
||||
action: "delete",
|
||||
},
|
||||
});
|
||||
corsStrategy: "all",
|
||||
},
|
||||
async ({ body, authentication }) => {
|
||||
try {
|
||||
const ingestionQueue = await getIngestionQueue(body.id);
|
||||
|
||||
if (!log) {
|
||||
throw new Response("Log not found", { status: 404 });
|
||||
}
|
||||
if (!ingestionQueue) {
|
||||
return json(
|
||||
{
|
||||
error: "Episode not found or unauthorized",
|
||||
code: "not_found",
|
||||
},
|
||||
{ status: 404 },
|
||||
);
|
||||
}
|
||||
|
||||
// Format the response
|
||||
const integrationDef =
|
||||
log.activity?.integrationAccount?.integrationDefinition;
|
||||
const logData = log.data as any;
|
||||
const output = ingestionQueue.output as any;
|
||||
const runningTasks = await runs.list({
|
||||
tag: [authentication.userId, ingestionQueue.id],
|
||||
taskIdentifier: "ingest-episode",
|
||||
});
|
||||
|
||||
const formattedLog = {
|
||||
id: log.id,
|
||||
source: integrationDef?.name || logData?.source || "Unknown",
|
||||
ingestText:
|
||||
log.activity?.text ||
|
||||
logData?.episodeBody ||
|
||||
logData?.text ||
|
||||
"No content",
|
||||
time: log.createdAt,
|
||||
processedAt: log.processedAt,
|
||||
episodeUUID: (log.output as any)?.episodeUuid,
|
||||
status: log.status,
|
||||
error: log.error,
|
||||
sourceURL: log.activity?.sourceURL,
|
||||
integrationSlug: integrationDef?.slug,
|
||||
data: log.data,
|
||||
};
|
||||
const latestTask = runningTasks.data.find(
|
||||
(task) =>
|
||||
task.tags.includes(authentication.userId) &&
|
||||
task.tags.includes(ingestionQueue.id),
|
||||
);
|
||||
|
||||
return json({ log: formattedLog });
|
||||
}
|
||||
if (latestTask && !latestTask?.isCompleted) {
|
||||
runs.cancel(latestTask?.id);
|
||||
}
|
||||
|
||||
let result;
|
||||
|
||||
if (output?.episodeUuid) {
|
||||
result = await deleteEpisodeWithRelatedNodes({
|
||||
episodeUuid: output?.episodeUuid,
|
||||
userId: authentication.userId,
|
||||
});
|
||||
|
||||
if (!result.episodeDeleted) {
|
||||
return json(
|
||||
{
|
||||
error: "Episode not found or unauthorized",
|
||||
code: "not_found",
|
||||
},
|
||||
{ status: 404 },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
await deleteIngestionQueue(ingestionQueue.id);
|
||||
|
||||
return json({
|
||||
success: true,
|
||||
message: "Episode deleted successfully",
|
||||
deleted: {
|
||||
episode: result?.episodeDeleted,
|
||||
statements: result?.statementsDeleted,
|
||||
entities: result?.entitiesDeleted,
|
||||
facts: result?.factsDeleted,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error deleting episode:", error);
|
||||
return json(
|
||||
{
|
||||
error: "Failed to delete episode",
|
||||
code: "internal_error",
|
||||
},
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
export { action, loader };
|
||||
|
||||
@ -15,7 +15,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
const url = new URL(request.url);
|
||||
|
||||
const page = parseInt(url.searchParams.get("page") || "1");
|
||||
const limit = parseInt(url.searchParams.get("limit") || "20");
|
||||
const limit = parseInt(url.searchParams.get("limit") || "100");
|
||||
const source = url.searchParams.get("source");
|
||||
const status = url.searchParams.get("status");
|
||||
const type = url.searchParams.get("type");
|
||||
@ -92,6 +92,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
createdAt: "desc",
|
||||
},
|
||||
skip,
|
||||
take: limit,
|
||||
}),
|
||||
|
||||
prisma.ingestionQueue.count({
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import { z } from "zod";
|
||||
import { json } from "@remix-run/node";
|
||||
import { createHybridActionApiRoute } from "~/services/routeBuilders/apiBuilder.server";
|
||||
import { uploadFileToS3 } from "~/lib/storage.server";
|
||||
@ -14,15 +13,6 @@ const { action, loader } = createHybridActionApiRoute(
|
||||
let fileName = "unnamed-file";
|
||||
let contentType = "application/octet-stream";
|
||||
|
||||
return json({
|
||||
success: true,
|
||||
|
||||
url: "http://localhost:3033/api/v1/storage/69bd1e11-552b-4708-91b0-bad006f41ddb",
|
||||
filename: fileName,
|
||||
|
||||
contentType: contentType,
|
||||
});
|
||||
|
||||
try {
|
||||
const contentTypeHeader = request.headers.get("Content-Type") || "";
|
||||
|
||||
|
||||
@ -3,6 +3,7 @@ import { useLoaderData } from "@remix-run/react";
|
||||
import { Inbox } from "lucide-react";
|
||||
import { PageHeader } from "~/components/common/page-header";
|
||||
import { LogDetails } from "~/components/logs/log-details";
|
||||
import { LogOptions } from "~/components/logs/log-options";
|
||||
|
||||
import { getIngestionQueueForFrontend } from "~/services/ingestionLogs.server";
|
||||
import { requireUserId } from "~/services/session.server";
|
||||
@ -11,9 +12,12 @@ export async function loader({ request, params }: LoaderFunctionArgs) {
|
||||
await requireUserId(request);
|
||||
const logId = params.logId;
|
||||
|
||||
const log = await getIngestionQueueForFrontend(logId as string);
|
||||
|
||||
return json({ log: log });
|
||||
try {
|
||||
const log = await getIngestionQueueForFrontend(logId as string);
|
||||
return json({ log: log });
|
||||
} catch (e) {
|
||||
return json({ log: null });
|
||||
}
|
||||
}
|
||||
|
||||
export default function InboxNotSelected() {
|
||||
@ -32,8 +36,12 @@ export default function InboxNotSelected() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full flex-col">
|
||||
<PageHeader title="Episode" showTrigger={false} />
|
||||
<div className="flex h-[calc(100vh_-_20px)] w-full flex-col overflow-hidden">
|
||||
<PageHeader
|
||||
title="Episode"
|
||||
showTrigger={false}
|
||||
actionsNode={<LogOptions id={log.id} />}
|
||||
/>
|
||||
|
||||
<LogDetails log={log as any} />
|
||||
</div>
|
||||
|
||||
@ -56,7 +56,7 @@ export class SpaceService {
|
||||
name: params.name.trim(),
|
||||
description: params.description?.trim(),
|
||||
workspaceId: params.workspaceId,
|
||||
status: "pending",
|
||||
status: "ready",
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@ -459,6 +459,81 @@ components:
|
||||
type: string
|
||||
format: date-time
|
||||
|
||||
Log:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
description: Log identifier
|
||||
source:
|
||||
type: string
|
||||
description: Log source name
|
||||
ingestText:
|
||||
type: string
|
||||
description: Text content that was ingested
|
||||
time:
|
||||
type: string
|
||||
format: date-time
|
||||
description: Creation timestamp
|
||||
processedAt:
|
||||
type: string
|
||||
format: date-time
|
||||
nullable: true
|
||||
description: Processing completion timestamp
|
||||
episodeUUID:
|
||||
type: string
|
||||
nullable: true
|
||||
description: Associated episode UUID if processed
|
||||
status:
|
||||
type: string
|
||||
enum: [pending, processing, completed, failed]
|
||||
description: Processing status
|
||||
error:
|
||||
type: string
|
||||
nullable: true
|
||||
description: Error message if processing failed
|
||||
sourceURL:
|
||||
type: string
|
||||
format: uri
|
||||
nullable: true
|
||||
description: Source URL if applicable
|
||||
integrationSlug:
|
||||
type: string
|
||||
nullable: true
|
||||
description: Integration slug identifier
|
||||
data:
|
||||
type: object
|
||||
nullable: true
|
||||
description: Additional log data
|
||||
|
||||
DeleteLogRequest:
|
||||
type: object
|
||||
required:
|
||||
- id
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
description: Log ID to delete
|
||||
|
||||
DeleteLogResponse:
|
||||
type: object
|
||||
properties:
|
||||
success:
|
||||
type: boolean
|
||||
message:
|
||||
type: string
|
||||
deleted:
|
||||
type: object
|
||||
properties:
|
||||
episode:
|
||||
type: boolean
|
||||
statements:
|
||||
type: integer
|
||||
entities:
|
||||
type: integer
|
||||
facts:
|
||||
type: integer
|
||||
|
||||
paths:
|
||||
# OAuth2 & Authentication
|
||||
/oauth/authorize:
|
||||
@ -1186,6 +1261,80 @@ paths:
|
||||
pagination:
|
||||
type: object
|
||||
|
||||
/api/v1/logs/{logId}:
|
||||
parameters:
|
||||
- name: logId
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
description: Log identifier
|
||||
|
||||
get:
|
||||
summary: Get Specific Log
|
||||
description: Retrieve detailed information about a specific ingestion log
|
||||
security:
|
||||
- bearerAuth: []
|
||||
responses:
|
||||
"200":
|
||||
description: Log details
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
log:
|
||||
$ref: "#/components/schemas/Log"
|
||||
"404":
|
||||
description: Log not found
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
"401":
|
||||
description: Unauthorized
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
|
||||
delete:
|
||||
summary: Delete Log Entry
|
||||
description: Delete a specific log entry and associated episode if it exists
|
||||
security:
|
||||
- bearerAuth: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/DeleteLogRequest"
|
||||
responses:
|
||||
"200":
|
||||
description: Log deleted successfully
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/DeleteLogResponse"
|
||||
"404":
|
||||
description: Log not found or unauthorized
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
"500":
|
||||
description: Internal server error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
"401":
|
||||
description: Unauthorized
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
|
||||
/api/v1/ingestion-queue/status:
|
||||
get:
|
||||
summary: Get Ingestion Queue Status
|
||||
|
||||
@ -459,6 +459,81 @@ components:
|
||||
type: string
|
||||
format: date-time
|
||||
|
||||
Log:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
description: Log identifier
|
||||
source:
|
||||
type: string
|
||||
description: Log source name
|
||||
ingestText:
|
||||
type: string
|
||||
description: Text content that was ingested
|
||||
time:
|
||||
type: string
|
||||
format: date-time
|
||||
description: Creation timestamp
|
||||
processedAt:
|
||||
type: string
|
||||
format: date-time
|
||||
nullable: true
|
||||
description: Processing completion timestamp
|
||||
episodeUUID:
|
||||
type: string
|
||||
nullable: true
|
||||
description: Associated episode UUID if processed
|
||||
status:
|
||||
type: string
|
||||
enum: [pending, processing, completed, failed]
|
||||
description: Processing status
|
||||
error:
|
||||
type: string
|
||||
nullable: true
|
||||
description: Error message if processing failed
|
||||
sourceURL:
|
||||
type: string
|
||||
format: uri
|
||||
nullable: true
|
||||
description: Source URL if applicable
|
||||
integrationSlug:
|
||||
type: string
|
||||
nullable: true
|
||||
description: Integration slug identifier
|
||||
data:
|
||||
type: object
|
||||
nullable: true
|
||||
description: Additional log data
|
||||
|
||||
DeleteLogRequest:
|
||||
type: object
|
||||
required:
|
||||
- id
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
description: Log ID to delete
|
||||
|
||||
DeleteLogResponse:
|
||||
type: object
|
||||
properties:
|
||||
success:
|
||||
type: boolean
|
||||
message:
|
||||
type: string
|
||||
deleted:
|
||||
type: object
|
||||
properties:
|
||||
episode:
|
||||
type: boolean
|
||||
statements:
|
||||
type: integer
|
||||
entities:
|
||||
type: integer
|
||||
facts:
|
||||
type: integer
|
||||
|
||||
paths:
|
||||
# OAuth2 & Authentication
|
||||
/oauth/authorize:
|
||||
@ -1536,6 +1611,80 @@ paths:
|
||||
pagination:
|
||||
type: object
|
||||
|
||||
/api/v1/logs/{logId}:
|
||||
parameters:
|
||||
- name: logId
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
description: Log identifier
|
||||
|
||||
get:
|
||||
summary: Get Specific Log
|
||||
description: Retrieve detailed information about a specific ingestion log
|
||||
security:
|
||||
- bearerAuth: []
|
||||
responses:
|
||||
"200":
|
||||
description: Log details
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
log:
|
||||
$ref: "#/components/schemas/Log"
|
||||
"404":
|
||||
description: Log not found
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
"401":
|
||||
description: Unauthorized
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
|
||||
delete:
|
||||
summary: Delete Log Entry
|
||||
description: Delete a specific log entry and associated episode if it exists
|
||||
security:
|
||||
- bearerAuth: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/DeleteLogRequest"
|
||||
responses:
|
||||
"200":
|
||||
description: Log deleted successfully
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/DeleteLogResponse"
|
||||
"404":
|
||||
description: Log not found or unauthorized
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
"500":
|
||||
description: Internal server error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
"401":
|
||||
description: Unauthorized
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
|
||||
/api/v1/ingestion-queue/status:
|
||||
get:
|
||||
summary: Get Ingestion Queue Status
|
||||
|
||||
@ -43,7 +43,7 @@ NEO4J_PASSWORD=27192e6432564f4788d55c15131bd5ac
|
||||
NEO4J_AUTH=neo4j/27192e6432564f4788d55c15131bd5ac
|
||||
|
||||
OPENAI_API_KEY=
|
||||
OLLAMA_URL=http://ollama:11434
|
||||
OLLAMA_URL=
|
||||
|
||||
EMBEDDING_MODEL=text-embedding-3-small
|
||||
MODEL=gpt-4.1-2025-04-14
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user