diff --git a/apps/webapp/app/components/common/styled-markdown.tsx b/apps/webapp/app/components/common/styled-markdown.tsx index b398b26..3b33881 100644 --- a/apps/webapp/app/components/common/styled-markdown.tsx +++ b/apps/webapp/app/components/common/styled-markdown.tsx @@ -1,5 +1,4 @@ -import ReactMarkdown from "react-markdown"; -import type { Components } from "react-markdown"; +import ReactMarkdown, {type Components } from "react-markdown"; import { cn } from "~/lib/utils"; const markdownComponents: Components = { diff --git a/apps/webapp/app/components/spaces/space-episode-actions.tsx b/apps/webapp/app/components/spaces/space-episode-actions.tsx new file mode 100644 index 0000000..f27c8fd --- /dev/null +++ b/apps/webapp/app/components/spaces/space-episode-actions.tsx @@ -0,0 +1,112 @@ +import { EllipsisVertical, Trash } from "lucide-react"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "../ui/dropdown-menu"; +import { Button } from "../ui/button"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "../ui/alert-dialog"; +import { useEffect, useState } from "react"; +import { useFetcher, useNavigate } from "@remix-run/react"; +import { toast } from "~/hooks/use-toast"; + +interface SpaceEpisodeActionsProps { + episodeId: string; + spaceId: string; +} + +export const SpaceEpisodeActions = ({ + episodeId, + spaceId, +}: SpaceEpisodeActionsProps) => { + const [removeDialogOpen, setRemoveDialogOpen] = useState(false); + const removeFetcher = useFetcher(); + const navigate = useNavigate(); + + const handleRemove = () => { + removeFetcher.submit( + { + episodeIds: JSON.stringify([episodeId]), + spaceId, + action: "remove", + }, + { + method: "post", + action: "/api/v1/episodes/assign-space", + encType: "application/json", + }, + ); + setRemoveDialogOpen(false); + }; + + useEffect(() => { + if (removeFetcher.state === "idle" && removeFetcher.data) { + if (removeFetcher.data.success) { + toast({ + title: "Success", + description: "Episode removed from space", + }); + // Reload the page to refresh the episode list + navigate(".", { replace: true }); + } else { + toast({ + title: "Error", + description: removeFetcher.data.error || "Failed to remove episode", + variant: "destructive", + }); + } + } + }, [removeFetcher.state, removeFetcher.data, navigate]); + + return ( + <> + + + + + + e.stopPropagation()}> + setRemoveDialogOpen(true)}> + + + + + + + e.stopPropagation()}> + + Remove from space + + Are you sure you want to remove this episode from the space? This + will not delete the episode itself. + + + + Cancel + + Remove + + + + + + ); +}; diff --git a/apps/webapp/app/components/spaces/space-episode-card.tsx b/apps/webapp/app/components/spaces/space-episode-card.tsx index 4c6e704..2272ceb 100644 --- a/apps/webapp/app/components/spaces/space-episode-card.tsx +++ b/apps/webapp/app/components/spaces/space-episode-card.tsx @@ -5,6 +5,7 @@ import { cn } from "~/lib/utils"; import { useNavigate } from "@remix-run/react"; import Markdown from "react-markdown"; import { StyledMarkdown } from "../common/styled-markdown"; +import { SpaceEpisodeActions } from "./space-episode-actions"; export interface Episode { uuid: string; @@ -20,9 +21,10 @@ export interface Episode { interface SpaceFactCardProps { episode: Episode; + spaceId: string; } -export function SpaceEpisodeCard({ episode }: SpaceFactCardProps) { +export function SpaceEpisodeCard({ episode, spaceId }: SpaceFactCardProps) { const navigate = useNavigate(); const formatDate = (date: Date | string) => { const d = new Date(date); @@ -62,6 +64,7 @@ export function SpaceEpisodeCard({ episode }: SpaceFactCardProps) { {formatDate(episode.validAt)} + diff --git a/apps/webapp/app/components/spaces/space-episodes-list.tsx b/apps/webapp/app/components/spaces/space-episodes-list.tsx index c26265f..4e18a3e 100644 --- a/apps/webapp/app/components/spaces/space-episodes-list.tsx +++ b/apps/webapp/app/components/spaces/space-episodes-list.tsx @@ -18,12 +18,14 @@ interface SpaceEpisodesListProps { loadMore: () => void; isLoading: boolean; height?: number; + spaceId: string; } function EpisodeItemRenderer( props: ListRowProps, episodes: Episode[], cache: CellMeasurerCache, + spaceId: string, ) { const { index, key, style, parent } = props; const episode = episodes[index]; @@ -37,7 +39,7 @@ function EpisodeItemRenderer( rowIndex={index} >
- +
); @@ -48,6 +50,7 @@ export function SpaceEpisodesList({ hasMore, loadMore, isLoading, + spaceId, }: SpaceEpisodesListProps) { // Create a CellMeasurerCache instance using useRef to prevent recreation const cacheRef = useRef(null); @@ -91,7 +94,7 @@ export function SpaceEpisodesList({ }; const rowRenderer = (props: ListRowProps) => { - return EpisodeItemRenderer(props, episodes, cache); + return EpisodeItemRenderer(props, episodes, cache, spaceId); }; const rowHeight = ({ index }: Index) => { diff --git a/apps/webapp/app/routes/home.space.$spaceId.episodes.tsx b/apps/webapp/app/routes/home.space.$spaceId.episodes.tsx index d5a8894..6017db1 100644 --- a/apps/webapp/app/routes/home.space.$spaceId.episodes.tsx +++ b/apps/webapp/app/routes/home.space.$spaceId.episodes.tsx @@ -37,7 +37,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) { } export default function Episodes() { - const { episodes } = useLoaderData(); + const { episodes, space } = useLoaderData(); const [selectedValidDate, setSelectedValidDate] = useState< string | undefined >(); @@ -98,6 +98,7 @@ export default function Episodes() { hasMore={false} // TODO: Implement real pagination loadMore={loadMore} isLoading={false} + spaceId={space.id} /> )} diff --git a/apps/webapp/app/services/space.server.ts b/apps/webapp/app/services/space.server.ts index b993174..10afc43 100644 --- a/apps/webapp/app/services/space.server.ts +++ b/apps/webapp/app/services/space.server.ts @@ -236,10 +236,6 @@ export class SpaceService { throw new Error("Space not found"); } - if (space.name === "Profile") { - throw new Error("Cannot reset Profile space"); - } - // Delete all relationships in Neo4j (episodes, statements, etc.) await deleteSpace(spaceId, userId); diff --git a/apps/webapp/app/trigger/spaces/space-summary.ts b/apps/webapp/app/trigger/spaces/space-summary.ts index 64d3c1c..3f95831 100644 --- a/apps/webapp/app/trigger/spaces/space-summary.ts +++ b/apps/webapp/app/trigger/spaces/space-summary.ts @@ -198,7 +198,7 @@ async function generateSpaceSummary( if ( episodeDifference < CONFIG.summaryEpisodeThreshold || - lastSummaryEpisodeCount === 0 + lastSummaryEpisodeCount !== 0 ) { logger.info( `Skipping summary generation for space ${spaceId}: only ${episodeDifference} new episodes (threshold: ${CONFIG.summaryEpisodeThreshold})`,