bump: new version 0.1.22

This commit is contained in:
Harshith Mullapudi 2025-09-15 10:54:47 +05:30
parent 8a4ada2c09
commit 019a5aaaaa
17 changed files with 324 additions and 163 deletions

View File

@ -1,4 +1,4 @@
VERSION=0.1.21
VERSION=0.1.22
# Nest run in docker, change host to database container name
DB_HOST=localhost

View File

@ -1,16 +1,65 @@
import { useState, useEffect } from "react";
import { useState, useEffect, 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 } from "../ui/badge";
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 { getStatusColor } from "./utils";
interface LogDetailsProps {
error?: string;
log: LogItem;
}
interface PropertyItemProps {
label: string;
value?: string | ReactNode;
icon?: ReactNode;
variant?: "default" | "secondary" | "outline" | "status";
statusColor?: string;
className?: string;
}
function PropertyItem({
label,
value,
icon,
variant = "secondary",
statusColor,
className,
}: PropertyItemProps) {
if (!value) return null;
return (
<div className="flex items-center py-1">
<span className="text-muted-foreground min-w-[160px]">{label}</span>
{variant === "status" ? (
<Badge
className={cn(
"!bg-grayAlpha-100 text-muted-foreground h-7 rounded px-4 text-xs",
className,
)}
>
{statusColor && (
<BadgeColor className={cn(statusColor, "h-2.5 w-2.5")} />
)}
{typeof value === "string"
? value.charAt(0).toUpperCase() + value.slice(1).toLowerCase()
: value}
</Badge>
) : (
<Badge variant={variant} className={cn("h-7 rounded px-4", className)}>
{icon}
{value}
</Badge>
)}
</div>
);
}
interface EpisodeFact {
uuid: string;
fact: string;
@ -24,7 +73,7 @@ interface EpisodeFactsResponse {
invalidFacts: EpisodeFact[];
}
export function LogDetails({ error, log }: LogDetailsProps) {
export function LogDetails({ log }: LogDetailsProps) {
const [facts, setFacts] = useState<any[]>([]);
const [invalidFacts, setInvalidFacts] = useState<any[]>([]);
const [factsLoading, setFactsLoading] = useState(false);
@ -32,11 +81,35 @@ export function LogDetails({ error, log }: LogDetailsProps) {
// Fetch episode facts when dialog opens and episodeUUID exists
useEffect(() => {
if (log.episodeUUID && facts.length === 0) {
setFactsLoading(true);
fetcher.load(`/api/v1/episodes/${log.episodeUUID}/facts`);
if (facts.length === 0) {
if (log.data?.type === "DOCUMENT" && log.data?.episodes?.length > 0) {
setFactsLoading(true);
// Fetch facts for all episodes in DOCUMENT type
Promise.all(
log.data.episodes.map((episodeId: string) =>
fetch(`/api/v1/episodes/${episodeId}/facts`).then((res) =>
res.json(),
),
),
)
.then((results) => {
const allFacts = results.flatMap((result) => result.facts || []);
const allInvalidFacts = results.flatMap(
(result) => result.invalidFacts || [],
);
setFacts(allFacts);
setInvalidFacts(allInvalidFacts);
setFactsLoading(false);
})
.catch(() => {
setFactsLoading(false);
});
} else if (log.episodeUUID) {
setFactsLoading(true);
fetcher.load(`/api/v1/episodes/${log.episodeUUID}/facts`);
}
}
}, [log.episodeUUID, facts.length]);
}, [log.episodeUUID, log.data?.type, log.data?.episodes, facts.length]);
// Handle fetcher response
useEffect(() => {
@ -49,37 +122,80 @@ export function LogDetails({ error, log }: LogDetailsProps) {
}, [fetcher.data, fetcher.state]);
return (
<div className="max-w-4xl">
<div className="px-4 pt-4">
<div className="mb-4 flex w-full items-center justify-between">
<span>Log Details</span>
<div className="flex gap-0.5">
{log.episodeUUID && (
<Badge variant="secondary" className="rounded text-xs">
Episode: {log.episodeUUID.slice(0, 8)}...
</Badge>
)}
{log.source && (
<Badge variant="secondary" className="rounded text-xs">
Source: {log.source}
</Badge>
)}
<div className="flex w-full flex-col items-center">
<div className="w-4xl">
<div className="px-4 pt-4">
<div className="mb-4 flex w-full items-center justify-between">
<span>Episode Details</span>
</div>
</div>
</div>
<div className="max-h-[90vh] 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 className="mb-10 px-4">
<div className="space-y-3">
{log.data?.type === "DOCUMENT" && log.data?.episodes ? (
<PropertyItem
label="Episodes"
value={
<div className="flex flex-wrap gap-1">
{log.data.episodes.map(
(episodeId: string, index: number) => (
<Badge
key={index}
variant="outline"
className="text-xs"
>
{episodeId}
</Badge>
),
)}
</div>
}
variant="secondary"
/>
) : (
<PropertyItem
label="Episode Id"
value={log.episodeUUID}
variant="secondary"
/>
)}
<PropertyItem
label="Session Id"
value={log.data?.sessionId?.toLowerCase()}
variant="secondary"
/>
<PropertyItem
label="Type"
value={
log.data?.type ? log.data.type.toLowerCase() : "conversation"
}
variant="secondary"
/>
<PropertyItem
label="Source"
value={log.source?.toLowerCase()}
icon={
log.source &&
getIconForAuthorise(log.source.toLowerCase(), 16, undefined)
}
variant="secondary"
/>
<PropertyItem
label="Status"
value={log.status}
variant="status"
statusColor={log.status && getStatusColor(log.status)}
/>
</div>
</div>
{/* Error Details */}
{log.error && (
<div className="mb-4">
<h3 className="mb-2 text-sm font-medium">Error Details</h3>
<div className="mb-6 px-4">
<div className="mb-2 flex w-full items-center justify-between">
<span>Error Details</span>
</div>
<div className="bg-destructive/10 rounded-md p-3">
<div className="flex items-start gap-2 text-red-600">
<AlertCircle className="mt-0.5 h-4 w-4 flex-shrink-0" />
@ -92,68 +208,77 @@ export function LogDetails({ error, log }: LogDetailsProps) {
)}
{/* Episode Facts */}
{log.episodeUUID && (
<div className="mb-4">
<h3 className="text-muted-foreground mb-2 text-sm">Facts</h3>
<div className="rounded-md">
{factsLoading ? (
<div className="flex items-center justify-center gap-2 p-4 text-sm">
<Loader2 className="h-4 w-4 animate-spin" />
</div>
) : facts.length > 0 ? (
<div className="flex flex-col gap-2">
{facts.map((fact) => (
<div
key={fact.uuid}
className="bg-grayAlpha-100 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">
<div className="mb-6 px-4">
<div className="mb-2 flex w-full items-center justify-between">
<span>Facts</span>
</div>
<div className="rounded-md">
{factsLoading ? (
<div className="flex items-center justify-center gap-2 p-4 text-sm">
<Loader2 className="h-4 w-4 animate-spin" />
</div>
) : facts.length > 0 ? (
<div className="flex flex-col gap-2">
{facts.map((fact) => (
<div
key={fact.uuid}
className="bg-grayAlpha-100 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">
<span>
Valid: {new Date(fact.validAt).toLocaleString()}
</span>
{fact.invalidAt && (
<span>
Valid: {new Date(fact.validAt).toLocaleString()}
Invalid: {new Date(fact.invalidAt).toLocaleString()}
</span>
{fact.invalidAt && (
<span>
Invalid: {new Date(fact.invalidAt).toLocaleString()}
</span>
)}
{Object.keys(fact.attributes).length > 0 && (
<Badge variant="secondary" className="text-xs">
{Object.keys(fact.attributes).length} attributes
</Badge>
)}
</div>
)}
{Object.keys(fact.attributes).length > 0 && (
<Badge variant="secondary" className="text-xs">
{Object.keys(fact.attributes).length} attributes
</Badge>
)}
</div>
))}
{invalidFacts.map((fact) => (
<div
key={fact.uuid}
className="bg-grayAlpha-100 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">
{fact.invalidAt && (
<span>
Invalid: {new Date(fact.invalidAt).toLocaleString()}
</span>
)}
{Object.keys(fact.attributes).length > 0 && (
<Badge variant="secondary" className="text-xs">
{Object.keys(fact.attributes).length} attributes
</Badge>
)}
</div>
</div>
))}
{invalidFacts.map((fact) => (
<div
key={fact.uuid}
className="bg-grayAlpha-100 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">
{fact.invalidAt && (
<span>
Invalid: {new Date(fact.invalidAt).toLocaleString()}
</span>
)}
{Object.keys(fact.attributes).length > 0 && (
<Badge variant="secondary" className="text-xs">
{Object.keys(fact.attributes).length} attributes
</Badge>
)}
</div>
))}
</div>
) : (
<div className="text-muted-foreground p-4 text-center text-sm">
No facts found for this episode
</div>
)}
</div>
))}
</div>
) : (
<div className="text-muted-foreground p-4 text-center text-sm">
No facts found for this episode
</div>
)}
</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>
);

View File

@ -3,6 +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";
interface LogTextCollapseProps {
text?: string;
@ -13,21 +14,6 @@ interface LogTextCollapseProps {
reset?: () => void;
}
const getStatusColor = (status: string) => {
switch (status) {
case "PROCESSING":
return "bg-blue-800";
case "PENDING":
return "bg-warning";
case "FAILED":
return "bg-destructive";
case "CANCELLED":
return "bg-gray-800";
default:
return "bg-gray-800";
}
};
export function LogTextCollapse({ text, log }: LogTextCollapseProps) {
const { logId } = useParams();
const navigate = useNavigate();

View File

@ -13,8 +13,10 @@ interface LogsFiltersProps {
availableSources: Array<{ name: string; slug: string }>;
selectedSource?: string;
selectedStatus?: string;
selectedType?: string;
onSourceChange: (source?: string) => void;
onStatusChange: (status?: string) => void;
onTypeChange: (type?: string) => void;
}
const statusOptions = [
@ -23,14 +25,21 @@ const statusOptions = [
{ value: "COMPLETED", label: "Completed" },
];
type FilterStep = "main" | "source" | "status";
const typeOptions = [
{ value: "CONVERSATION", label: "Conversation" },
{ value: "DOCUMENT", label: "Document" },
];
type FilterStep = "main" | "source" | "status" | "type";
export function LogsFilters({
availableSources,
selectedSource,
selectedStatus,
selectedType,
onSourceChange,
onStatusChange,
onTypeChange,
}: LogsFiltersProps) {
const [popoverOpen, setPopoverOpen] = useState(false);
const [step, setStep] = useState<FilterStep>("main");
@ -44,8 +53,11 @@ export function LogsFilters({
const selectedStatusLabel = statusOptions.find(
(s) => s.value === selectedStatus,
)?.label;
const selectedTypeLabel = typeOptions.find(
(s) => s.value === selectedType,
)?.label;
const hasFilters = selectedSource || selectedStatus;
const hasFilters = selectedSource || selectedStatus || selectedType;
return (
<div className="mb-2 flex w-full items-center justify-start gap-2 px-3">
@ -85,6 +97,13 @@ export function LogsFilters({
>
Status
</Button>
<Button
variant="ghost"
className="justify-start"
onClick={() => setStep("type")}
>
Type
</Button>
</div>
)}
@ -155,6 +174,40 @@ export function LogsFilters({
))}
</div>
)}
{step === "type" && (
<div className="flex flex-col gap-1 p-2">
<Button
variant="ghost"
className="w-full justify-start"
onClick={() => {
onTypeChange(undefined);
setPopoverOpen(false);
setStep("main");
}}
>
All types
</Button>
{typeOptions.map((type) => (
<Button
key={type.value}
variant="ghost"
className="w-full justify-start"
onClick={() => {
onTypeChange(
type.value === selectedType
? undefined
: type.value,
);
setPopoverOpen(false);
setStep("main");
}}
>
{type.label}
</Button>
))}
</div>
)}
</PopoverContent>
</PopoverPortal>
</Popover>
@ -180,6 +233,15 @@ export function LogsFilters({
/>
</Badge>
)}
{selectedType && (
<Badge variant="secondary" className="h-7 gap-1 rounded px-2">
{selectedTypeLabel}
<X
className="hover:text-destructive h-3.5 w-3.5 cursor-pointer"
onClick={() => onTypeChange(undefined)}
/>
</Badge>
)}
</div>
)}
</div>

View File

@ -0,0 +1,16 @@
export const getStatusColor = (status: string) => {
switch (status) {
case "PROCESSING":
return "bg-blue-800";
case "PENDING":
return "bg-warning";
case "COMPLETED":
return "bg-success";
case "FAILED":
return "bg-destructive";
case "CANCELLED":
return "bg-gray-800";
default:
return "bg-gray-800";
}
};

View File

@ -8,7 +8,7 @@ import { extensionsForConversation } from "../conversation/editor-extensions";
export const SpaceSummary = ({ summary }: { summary?: string | null }) => {
const editor = useEditor({
extensions: [...extensionsForConversation, skillExtension],
editable: true,
editable: false,
content: summary,
});

View File

@ -67,7 +67,7 @@ const EnvironmentSchema = z.object({
//OpenAI
OPENAI_API_KEY: z.string(),
EMAIL_TRANSPORT: z.enum(["resend", "smtp", "aws-ses"]).optional(),
EMAIL_TRANSPORT: z.string().optional(),
FROM_EMAIL: z.string().optional(),
REPLY_TO_EMAIL: z.string().optional(),
RESEND_API_KEY: z.string().optional(),

View File

@ -30,9 +30,10 @@ export interface UseLogsOptions {
endpoint: string; // '/api/v1/logs/all' or '/api/v1/logs/activity'
source?: string;
status?: string;
type?: string;
}
export function useLogs({ endpoint, source, status }: UseLogsOptions) {
export function useLogs({ endpoint, source, status, type }: UseLogsOptions) {
const fetcher = useFetcher<LogsResponse>();
const [logs, setLogs] = useState<LogItem[]>([]);
const [page, setPage] = useState(1);
@ -49,9 +50,10 @@ export function useLogs({ endpoint, source, status }: UseLogsOptions) {
params.set("limit", "5");
if (source) params.set("source", source);
if (status) params.set("status", status);
if (type) params.set("type", type);
return `${endpoint}?${params.toString()}`;
},
[endpoint, source, status],
[endpoint, source, status, type],
);
const loadMore = useCallback(() => {
@ -100,7 +102,7 @@ export function useLogs({ endpoint, source, status }: UseLogsOptions) {
setHasMore(true);
setIsInitialLoad(true);
fetcher.load(buildUrl(1));
}, [source, status, buildUrl]); // Inline reset logic to avoid dependency issues
}, [source, status, type, buildUrl]); // Inline reset logic to avoid dependency issues
// Initial load
useEffect(() => {

View File

@ -18,6 +18,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
const limit = parseInt(url.searchParams.get("limit") || "20");
const source = url.searchParams.get("source");
const status = url.searchParams.get("status");
const type = url.searchParams.get("type");
const skip = (page - 1) * limit;
// Get user and workspace in one query
@ -39,6 +40,13 @@ export async function loader({ request }: LoaderFunctionArgs) {
whereClause.status = status;
}
if (type) {
whereClause.data = {
path: ["type"],
equals: type,
};
}
// If source filter is provided, filter by integration source
if (source) {
whereClause.activity = {

View File

@ -16,6 +16,7 @@ import { cn } from "~/lib/utils";
export default function LogsAll() {
const [selectedSource, setSelectedSource] = useState<string | undefined>();
const [selectedStatus, setSelectedStatus] = useState<string | undefined>();
const [selectedType, setSelectedType] = useState<string | undefined>();
const { logId } = useParams();
@ -30,6 +31,7 @@ export default function LogsAll() {
endpoint: "/api/v1/logs",
source: selectedSource,
status: selectedStatus,
type: selectedType,
});
return (
@ -37,15 +39,15 @@ export default function LogsAll() {
<ResizablePanelGroup direction="horizontal">
<ResizablePanel
maxSize={50}
defaultSize={35}
minSize={35}
defaultSize={30}
minSize={30}
collapsible
collapsedSize={35}
collapsedSize={30}
>
<div className="flex h-full flex-col">
<PageHeader title="Inbox" />
<div className="flex h-[calc(100vh_-_56px)] w-full flex-col items-center space-y-6 pt-3 pb-4">
<div className="flex h-[calc(100vh_-_56px)] w-full flex-col items-center space-y-6 pt-3">
{isInitialLoad ? (
<>
<LoaderCircle className="text-primary h-4 w-4 animate-spin" />
@ -58,12 +60,14 @@ export default function LogsAll() {
availableSources={availableSources}
selectedSource={selectedSource}
selectedStatus={selectedStatus}
selectedType={selectedType}
onSourceChange={setSelectedSource}
onStatusChange={setSelectedStatus}
onTypeChange={setSelectedType}
/>
{/* Logs List */}
<div className="flex h-full w-full space-y-4">
<div className="flex h-full w-full space-y-4 pb-2">
{logs.length === 0 ? (
<Card className="bg-background-2 w-full">
<CardContent className="bg-background-2 flex w-full items-center justify-center py-16">
@ -73,7 +77,7 @@ export default function LogsAll() {
No logs found
</h3>
<p className="text-muted-foreground">
{selectedSource || selectedStatus
{selectedSource || selectedStatus || selectedType
? "Try adjusting your filters to see more results."
: "No ingestion logs are available yet."}
</p>

View File

@ -88,7 +88,7 @@ export default function Facts() {
onSpaceFilterChange={setSelectedSpaceFilter}
/>
<div className="flex h-[calc(100vh_-_140px)] w-full">
<div className="flex h-[calc(100vh_-_56px)] w-full">
<ClientOnly
fallback={<LoaderCircle className="mr-2 h-4 w-4 animate-spin" />}
>

View File

@ -88,7 +88,7 @@ export default function Patterns() {
return (
<div className="flex h-full w-full flex-col pt-2">
<div className="flex h-[calc(100vh_-_140px)] w-full">
<div className="flex h-[calc(100vh_-_56px)] w-full">
<ClientOnly
fallback={<LoaderCircle className="mr-2 h-4 w-4 animate-spin" />}
>

View File

@ -43,44 +43,3 @@ export async function getEpisodeFacts(episodeUuid: string, userId: string) {
};
}
}
export async function getDocumentFacts(documentId: string, userId: string) {
try {
const facts = await getEpisodeStatements({
episodeUuid,
userId,
});
const invalidFacts = await getStatementsInvalidatedByEpisode({
episodeUuid,
userId,
});
return {
success: true,
facts: facts.map((fact) => ({
uuid: fact.uuid,
fact: fact.fact,
createdAt: fact.createdAt.toISOString(),
validAt: fact.validAt.toISOString(),
invalidAt: fact.invalidAt ? fact.invalidAt.toISOString() : null,
attributes: fact.attributes,
})),
invalidFacts: invalidFacts.map((fact) => ({
uuid: fact.uuid,
fact: fact.fact,
createdAt: fact.createdAt.toISOString(),
validAt: fact.validAt.toISOString(),
invalidAt: fact.invalidAt ? fact.invalidAt.toISOString() : null,
attributes: fact.attributes,
})),
};
} catch (error) {
console.error("Error fetching episode facts:", error);
return {
success: false,
error: "Failed to fetch episode facts",
facts: [],
};
}
}

View File

@ -1,5 +1,5 @@
import { runQuery } from "~/lib/neo4j.server";
import { type EntityNode, EpisodeType, type EpisodicNode } from "@core/types";
import { type EntityNode, type EpisodicNode } from "@core/types";
export async function saveEpisode(episode: EpisodicNode): Promise<string> {
const query = `

View File

@ -14,7 +14,6 @@ import { callMemoryTool, memoryTools } from "~/utils/mcp/memory";
import { logger } from "~/services/logger.service";
import { type Response, type Request } from "express";
import { getWorkspaceByUser } from "~/models/workspace.server";
import { Workspace } from "@prisma/client";
const QueryParams = z.object({
source: z.string().optional(),

View File

@ -1,4 +1,4 @@
VERSION=0.1.21
VERSION=0.1.22
# Nest run in docker, change host to database container name
DB_HOST=postgres

View File

@ -1,7 +1,7 @@
{
"name": "core",
"private": true,
"version": "0.1.21",
"version": "0.1.22",
"workspaces": [
"apps/*",
"packages/*"