Add OpenMemory (#2676)
Co-authored-by: Saket Aryan <94069182+whysosaket@users.noreply.github.com> Co-authored-by: Saket Aryan <saketaryan2002@gmail.com>
This commit is contained in:
166
openmemory/ui/app/apps/[appId]/components/AppDetailCard.tsx
Normal file
166
openmemory/ui/app/apps/[appId]/components/AppDetailCard.tsx
Normal file
@@ -0,0 +1,166 @@
|
||||
import React, { useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { PauseIcon, Loader2, PlayIcon } from "lucide-react";
|
||||
import { useAppsApi } from "@/hooks/useAppsApi";
|
||||
import Image from "next/image";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { setAppDetails } from "@/store/appsSlice";
|
||||
import { BiEdit } from "react-icons/bi";
|
||||
import { constants } from "@/components/shared/source-app";
|
||||
import { RootState } from "@/store/store";
|
||||
|
||||
const capitalize = (str: string) => {
|
||||
return str.charAt(0).toUpperCase() + str.slice(1);
|
||||
};
|
||||
|
||||
const AppDetailCard = ({
|
||||
appId,
|
||||
selectedApp,
|
||||
}: {
|
||||
appId: string;
|
||||
selectedApp: any;
|
||||
}) => {
|
||||
const { updateAppDetails } = useAppsApi();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const dispatch = useDispatch();
|
||||
const apps = useSelector((state: RootState) => state.apps.apps);
|
||||
const currentApp = apps.find((app: any) => app.id === appId);
|
||||
const appConfig = currentApp
|
||||
? constants[currentApp.name as keyof typeof constants] || constants.default
|
||||
: constants.default;
|
||||
|
||||
const handlePauseAccess = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
await updateAppDetails(appId, {
|
||||
is_active: !selectedApp.details.is_active,
|
||||
});
|
||||
dispatch(
|
||||
setAppDetails({ appId, isActive: !selectedApp.details.is_active })
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Failed to toggle app pause state:", error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const buttonText = selectedApp.details.is_active
|
||||
? "Pause Access"
|
||||
: "Unpause Access";
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="bg-zinc-900 border w-[320px] border-zinc-800 rounded-xl mb-6">
|
||||
<div className="flex items-center gap-2 mb-4 bg-zinc-800 rounded-t-xl p-3">
|
||||
<div className="w-5 h-5 flex items-center justify-center">
|
||||
{appConfig.iconImage ? (
|
||||
<div>
|
||||
<div className="w-6 h-6 rounded-full bg-zinc-700 flex items-center justify-center overflow-hidden">
|
||||
<Image
|
||||
src={appConfig.iconImage}
|
||||
alt={appConfig.name}
|
||||
width={40}
|
||||
height={40}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="w-5 h-5 flex items-center justify-center bg-zinc-700 rounded-full">
|
||||
<BiEdit className="w-4 h-4 text-zinc-400" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<h2 className="text-md font-semibold">{appConfig.name}</h2>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4 p-3">
|
||||
<div>
|
||||
<p className="text-xs text-zinc-400">Access Status</p>
|
||||
<p
|
||||
className={`font-medium ${
|
||||
selectedApp.details.is_active
|
||||
? "text-emerald-500"
|
||||
: "text-red-500"
|
||||
}`}
|
||||
>
|
||||
{capitalize(
|
||||
selectedApp.details.is_active ? "active" : "inactive"
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="text-xs text-zinc-400">Total Memories Created</p>
|
||||
<p className="font-medium">
|
||||
{selectedApp.details.total_memories_created} Memories
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="text-xs text-zinc-400">Total Memories Accessed</p>
|
||||
<p className="font-medium">
|
||||
{selectedApp.details.total_memories_accessed} Memories
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="text-xs text-zinc-400">First Accessed</p>
|
||||
<p className="font-medium">
|
||||
{selectedApp.details.first_accessed
|
||||
? new Date(
|
||||
selectedApp.details.first_accessed
|
||||
).toLocaleDateString("en-US", {
|
||||
day: "numeric",
|
||||
month: "short",
|
||||
year: "numeric",
|
||||
hour: "numeric",
|
||||
minute: "numeric",
|
||||
})
|
||||
: "Never"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="text-xs text-zinc-400">Last Accessed</p>
|
||||
<p className="font-medium">
|
||||
{selectedApp.details.last_accessed
|
||||
? new Date(
|
||||
selectedApp.details.last_accessed
|
||||
).toLocaleDateString("en-US", {
|
||||
day: "numeric",
|
||||
month: "short",
|
||||
year: "numeric",
|
||||
hour: "numeric",
|
||||
minute: "numeric",
|
||||
})
|
||||
: "Never"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<hr className="border-zinc-800" />
|
||||
|
||||
<div className="flex gap-2 justify-end">
|
||||
<Button
|
||||
onClick={handlePauseAccess}
|
||||
className="flex bg-transparent w-[170px] bg-zinc-800 border-zinc-800 hover:bg-zinc-800 text-white"
|
||||
size="sm"
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : buttonText === "Pause Access" ? (
|
||||
<PauseIcon className="h-4 w-4" />
|
||||
) : (
|
||||
<PlayIcon className="h-4 w-4" />
|
||||
)}
|
||||
{buttonText}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AppDetailCard;
|
||||
115
openmemory/ui/app/apps/[appId]/components/MemoryCard.tsx
Normal file
115
openmemory/ui/app/apps/[appId]/components/MemoryCard.tsx
Normal file
@@ -0,0 +1,115 @@
|
||||
import { ArrowRight } from "lucide-react";
|
||||
import Categories from "@/components/shared/categories";
|
||||
import Link from "next/link";
|
||||
import { constants } from "@/components/shared/source-app";
|
||||
import Image from "next/image";
|
||||
interface MemoryCardProps {
|
||||
id: string;
|
||||
content: string;
|
||||
created_at: string;
|
||||
metadata?: Record<string, any>;
|
||||
categories?: string[];
|
||||
access_count?: number;
|
||||
app_name: string;
|
||||
state: string;
|
||||
}
|
||||
|
||||
export function MemoryCard({
|
||||
id,
|
||||
content,
|
||||
created_at,
|
||||
metadata,
|
||||
categories,
|
||||
access_count,
|
||||
app_name,
|
||||
state,
|
||||
}: MemoryCardProps) {
|
||||
return (
|
||||
<div className="rounded-lg border border-zinc-800 bg-zinc-900 overflow-hidden">
|
||||
<div className="p-4">
|
||||
<div className="border-l-2 border-primary pl-4 mb-4">
|
||||
<p
|
||||
className={`${state !== "active" ? "text-zinc-400" : "text-white"}`}
|
||||
>
|
||||
{content}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{metadata && Object.keys(metadata).length > 0 && (
|
||||
<div className="mb-4">
|
||||
<p className="text-xs text-zinc-500 uppercase mb-2">METADATA</p>
|
||||
<div className="bg-zinc-800 rounded p-3 text-zinc-400">
|
||||
<pre className="whitespace-pre-wrap">
|
||||
{JSON.stringify(metadata, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mb-2">
|
||||
<Categories
|
||||
categories={categories as any}
|
||||
isPaused={state !== "active"}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-zinc-400 text-sm">
|
||||
{access_count ? (
|
||||
<span className="relative top-1">
|
||||
Accessed {access_count} times
|
||||
</span>
|
||||
) : (
|
||||
new Date(created_at + "Z").toLocaleDateString("en-US", {
|
||||
year: "numeric",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
hour: "numeric",
|
||||
minute: "numeric",
|
||||
})
|
||||
)}
|
||||
</span>
|
||||
|
||||
{state !== "active" && (
|
||||
<span className="inline-block px-3 border border-yellow-600 text-yellow-600 font-semibold text-xs rounded-full bg-yellow-400/10 backdrop-blur-sm">
|
||||
{state === "paused" ? "Paused" : "Archived"}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!app_name && (
|
||||
<Link
|
||||
href={`/memory/${id}`}
|
||||
className="hover:cursor-pointer bg-zinc-800 hover:bg-zinc-700 flex items-center px-3 py-1 text-sm rounded-lg text-white p-0 hover:text-white"
|
||||
>
|
||||
View Details
|
||||
<ArrowRight className="ml-2 h-4 w-4" />
|
||||
</Link>
|
||||
)}
|
||||
{app_name && (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-1 bg-zinc-700 px-3 py-1 rounded-lg">
|
||||
<span className="text-sm text-zinc-400">Created by:</span>
|
||||
<div className="w-5 h-5 rounded-full bg-zinc-700 flex items-center justify-center overflow-hidden">
|
||||
<Image
|
||||
src={
|
||||
constants[app_name as keyof typeof constants]
|
||||
?.iconImage || ""
|
||||
}
|
||||
alt="OpenMemory"
|
||||
width={24}
|
||||
height={24}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-sm text-zinc-100 font-semibold">
|
||||
{constants[app_name as keyof typeof constants]?.name}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
219
openmemory/ui/app/apps/[appId]/page.tsx
Normal file
219
openmemory/ui/app/apps/[appId]/page.tsx
Normal file
@@ -0,0 +1,219 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useParams } from "next/navigation";
|
||||
import { useSelector } from "react-redux";
|
||||
import { RootState } from "@/store/store";
|
||||
import { useAppsApi } from "@/hooks/useAppsApi";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { MemoryCard } from "./components/MemoryCard";
|
||||
import AppDetailCard from "./components/AppDetailCard";
|
||||
import "@/styles/animation.css";
|
||||
import NotFound from "@/app/not-found";
|
||||
import { AppDetailCardSkeleton } from "@/skeleton/AppDetailCardSkeleton";
|
||||
import { MemoryCardSkeleton } from "@/skeleton/MemoryCardSkeleton";
|
||||
|
||||
export default function AppDetailsPage() {
|
||||
const params = useParams();
|
||||
const appId = params.appId as string;
|
||||
const [activeTab, setActiveTab] = useState("created");
|
||||
|
||||
const {
|
||||
fetchAppDetails,
|
||||
fetchAppMemories,
|
||||
fetchAppAccessedMemories,
|
||||
fetchApps,
|
||||
} = useAppsApi();
|
||||
const selectedApp = useSelector((state: RootState) => state.apps.selectedApp);
|
||||
|
||||
useEffect(() => {
|
||||
fetchApps({});
|
||||
}, [fetchApps]);
|
||||
|
||||
useEffect(() => {
|
||||
const loadData = async () => {
|
||||
if (appId) {
|
||||
try {
|
||||
// Load all data in parallel
|
||||
await Promise.all([
|
||||
fetchAppDetails(appId),
|
||||
fetchAppMemories(appId),
|
||||
fetchAppAccessedMemories(appId),
|
||||
]);
|
||||
} catch (error) {
|
||||
console.error("Error loading app data:", error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
loadData();
|
||||
}, [appId, fetchAppDetails, fetchAppMemories, fetchAppAccessedMemories]);
|
||||
|
||||
if (selectedApp.error) {
|
||||
return (
|
||||
<NotFound message={selectedApp.error} title="Error loading app details" />
|
||||
);
|
||||
}
|
||||
|
||||
if (!selectedApp.details) {
|
||||
return (
|
||||
<div className="flex-1 py-6 text-white">
|
||||
<div className="container flex justify-between">
|
||||
<div className="flex-1 p-4 max-w-4xl animate-fade-slide-down">
|
||||
<div className="mb-6">
|
||||
<div className="h-10 w-64 bg-zinc-800 rounded animate-pulse mb-6" />
|
||||
<div className="space-y-6">
|
||||
{[...Array(3)].map((_, i) => (
|
||||
<MemoryCardSkeleton key={i} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-14 animate-fade-slide-down delay-2">
|
||||
<AppDetailCardSkeleton />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const renderCreatedMemories = () => {
|
||||
const memories = selectedApp.memories.created;
|
||||
|
||||
if (memories.loading) {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{[...Array(3)].map((_, i) => (
|
||||
<MemoryCardSkeleton key={i} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (memories.error) {
|
||||
return (
|
||||
<NotFound message={memories.error} title="Error loading memories" />
|
||||
);
|
||||
}
|
||||
|
||||
if (memories.items.length === 0) {
|
||||
return (
|
||||
<div className="text-zinc-400 text-center py-8">No memories found</div>
|
||||
);
|
||||
}
|
||||
|
||||
return memories.items.map((memory) => (
|
||||
<MemoryCard
|
||||
key={memory.id + memory.created_at}
|
||||
id={memory.id}
|
||||
content={memory.content}
|
||||
created_at={memory.created_at}
|
||||
metadata={memory.metadata_}
|
||||
categories={memory.categories}
|
||||
app_name={memory.app_name}
|
||||
state={memory.state}
|
||||
/>
|
||||
));
|
||||
};
|
||||
|
||||
const renderAccessedMemories = () => {
|
||||
const memories = selectedApp.memories.accessed;
|
||||
|
||||
if (memories.loading) {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{[...Array(3)].map((_, i) => (
|
||||
<MemoryCardSkeleton key={i} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (memories.error) {
|
||||
return (
|
||||
<div className="text-red-400 bg-red-400/10 p-4 rounded-lg">
|
||||
Error loading memories: {memories.error}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (memories.items.length === 0) {
|
||||
return (
|
||||
<div className="text-zinc-400 text-center py-8">
|
||||
No accessed memories found
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return memories.items.map((accessedMemory) => (
|
||||
<div
|
||||
key={accessedMemory.memory.id + accessedMemory.memory.created_at}
|
||||
className="relative"
|
||||
>
|
||||
<MemoryCard
|
||||
id={accessedMemory.memory.id}
|
||||
content={accessedMemory.memory.content}
|
||||
created_at={accessedMemory.memory.created_at}
|
||||
metadata={accessedMemory.memory.metadata_}
|
||||
categories={accessedMemory.memory.categories}
|
||||
access_count={accessedMemory.access_count}
|
||||
app_name={accessedMemory.memory.app_name}
|
||||
state={accessedMemory.memory.state}
|
||||
/>
|
||||
</div>
|
||||
));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex-1 py-6 text-white">
|
||||
<div className="container flex justify-between">
|
||||
{/* Main content area */}
|
||||
<div className="flex-1 p-4 max-w-4xl animate-fade-slide-down">
|
||||
<Tabs
|
||||
defaultValue="created"
|
||||
className="mb-6"
|
||||
onValueChange={setActiveTab}
|
||||
>
|
||||
<TabsList className="bg-transparent border-b border-zinc-800 rounded-none w-full justify-start gap-8 p-0">
|
||||
<TabsTrigger
|
||||
value="created"
|
||||
className={`px-0 pb-2 rounded-none data-[state=active]:border-b-2 data-[state=active]:border-primary data-[state=active]:shadow-none ${
|
||||
activeTab === "created" ? "text-white" : "text-zinc-400"
|
||||
}`}
|
||||
>
|
||||
Created ({selectedApp.memories.created.total})
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="accessed"
|
||||
className={`px-0 pb-2 rounded-none data-[state=active]:border-b-2 data-[state=active]:border-primary data-[state=active]:shadow-none ${
|
||||
activeTab === "accessed" ? "text-white" : "text-zinc-400"
|
||||
}`}
|
||||
>
|
||||
Accessed ({selectedApp.memories.accessed.total})
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent
|
||||
value="created"
|
||||
className="mt-6 space-y-6 animate-fade-slide-down delay-1"
|
||||
>
|
||||
{renderCreatedMemories()}
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent
|
||||
value="accessed"
|
||||
className="mt-6 space-y-6 animate-fade-slide-down delay-1"
|
||||
>
|
||||
{renderAccessedMemories()}
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
|
||||
{/* Sidebar */}
|
||||
<div className="p-14 animate-fade-slide-down delay-2">
|
||||
<AppDetailCard appId={appId} selectedApp={selectedApp} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user