mirror of
https://github.com/aljazceru/claude-code-viewer.git
synced 2025-12-26 09:44:21 +01:00
feat: implement sessions sidebar
This commit is contained in:
@@ -11,5 +11,7 @@ export const useProject = (projectId: string) => {
|
||||
|
||||
return await response.json();
|
||||
},
|
||||
refetchOnReconnect: true,
|
||||
refetchInterval: 10 * 1000,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -7,6 +7,7 @@ import { Button } from "@/components/ui/button";
|
||||
import { firstCommandToTitle } from "../../../services/firstCommandToTitle";
|
||||
import { useSession } from "../hooks/useSession";
|
||||
import { ConversationList } from "./conversationList/ConversationList";
|
||||
import { SessionSidebar } from "./sessionSidebar/SessionSidebar";
|
||||
|
||||
export const SessionPageContent: FC<{
|
||||
projectId: string;
|
||||
@@ -18,37 +19,43 @@ export const SessionPageContent: FC<{
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<header className="mb-8">
|
||||
<Button asChild variant="ghost" className="mb-4">
|
||||
<Link
|
||||
href={`/projects/${projectId}`}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<ArrowLeftIcon className="w-4 h-4" />
|
||||
Back to Session List
|
||||
</Link>
|
||||
</Button>
|
||||
<div className="flex h-screen">
|
||||
<SessionSidebar currentSessionId={sessionId} projectId={projectId} />
|
||||
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<MessageSquareIcon className="w-6 h-6" />
|
||||
<h1 className="text-3xl font-bold">
|
||||
{session.meta.firstCommand !== null
|
||||
? firstCommandToTitle(session.meta.firstCommand)
|
||||
: sessionId}
|
||||
</h1>
|
||||
<div className="flex-1 flex flex-col overflow-hidden">
|
||||
<div className="max-w-none px-6 md:px-8 py-6 md:py-8 flex-1 overflow-y-auto">
|
||||
<header className="mb-8">
|
||||
<Button asChild variant="ghost" className="mb-4">
|
||||
<Link
|
||||
href={`/projects/${projectId}`}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<ArrowLeftIcon className="w-4 h-4" />
|
||||
Back to Session List
|
||||
</Link>
|
||||
</Button>
|
||||
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<MessageSquareIcon className="w-6 h-6" />
|
||||
<h1 className="text-3xl font-bold">
|
||||
{session.meta.firstCommand !== null
|
||||
? firstCommandToTitle(session.meta.firstCommand)
|
||||
: sessionId}
|
||||
</h1>
|
||||
</div>
|
||||
<p className="text-muted-foreground font-mono text-sm">
|
||||
Session ID: {sessionId}
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<main className="w-full px-20">
|
||||
<ConversationList
|
||||
conversations={conversations}
|
||||
getToolResult={getToolResult}
|
||||
/>
|
||||
</main>
|
||||
</div>
|
||||
<p className="text-muted-foreground font-mono text-sm">
|
||||
Session ID: {sessionId}
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<ConversationList
|
||||
conversations={conversations}
|
||||
getToolResult={getToolResult}
|
||||
/>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -51,28 +51,22 @@ export const ConversationList: FC<ConversationListProps> = ({
|
||||
/>
|
||||
);
|
||||
|
||||
const isSidechain =
|
||||
conversation.type !== "summary" && conversation.isSidechain;
|
||||
|
||||
return [
|
||||
<li
|
||||
className={`w-full flex ${
|
||||
conversation.type === "user"
|
||||
? "justify-end"
|
||||
: conversation.type === "assistant"
|
||||
? "justify-start"
|
||||
: "justify-center"
|
||||
isSidechain ||
|
||||
conversation.type === "assistant" ||
|
||||
conversation.type === "system" ||
|
||||
conversation.type === "summary"
|
||||
? "justify-start"
|
||||
: "justify-end"
|
||||
}`}
|
||||
key={getConversationKey(conversation)}
|
||||
>
|
||||
<div
|
||||
className={`${
|
||||
conversation.type === "user"
|
||||
? "w-[90%]"
|
||||
: conversation.type === "assistant"
|
||||
? "w-[90%]"
|
||||
: "w-[100%]"
|
||||
}`}
|
||||
>
|
||||
{elm}
|
||||
</div>
|
||||
<div className="w-[85%]">{elm}</div>
|
||||
</li>,
|
||||
];
|
||||
})}
|
||||
|
||||
@@ -63,7 +63,11 @@ export const SidechainConversationModal: FC<
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline" size="sm" className="w-full mb-3">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="w-full mb-3 items-center justify-start"
|
||||
>
|
||||
<div className="flex items-center gap-2 overflow-hidden">
|
||||
<Eye className="h-4 w-4 flex-shrink-0" />
|
||||
<span className="overflow-hidden text-ellipsis">
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
"use client";
|
||||
|
||||
import { MessageSquareIcon, PanelLeftIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { type FC, useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from "@/components/ui/collapsible";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { Session } from "../../../../../../../server/service/types";
|
||||
import { useProject } from "../../../../hooks/useProject";
|
||||
import { firstCommandToTitle } from "../../../../services/firstCommandToTitle";
|
||||
|
||||
const SidebarContent: FC<{
|
||||
sessions: Session[];
|
||||
currentSessionId: string;
|
||||
projectId: string;
|
||||
}> = ({ sessions, currentSessionId, projectId }) => (
|
||||
<div className="h-full flex flex-col bg-sidebar text-sidebar-foreground">
|
||||
<div className="border-b border-sidebar-border p-4">
|
||||
<h2 className="font-semibold text-lg">Sessions</h2>
|
||||
<p className="text-xs text-sidebar-foreground/70 mt-1">
|
||||
{sessions.length} total
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto p-2 space-y-0.5">
|
||||
{sessions.map((session) => {
|
||||
const isActive = session.id === currentSessionId;
|
||||
const title =
|
||||
session.meta.firstCommand !== null
|
||||
? firstCommandToTitle(session.meta.firstCommand)
|
||||
: session.id;
|
||||
|
||||
return (
|
||||
<Link
|
||||
key={session.id}
|
||||
href={`/projects/${projectId}/sessions/${encodeURIComponent(
|
||||
session.id,
|
||||
)}`}
|
||||
className={cn(
|
||||
"block rounded-lg p-2.5 transition-all duration-200 hover:bg-blue-50/60 hover:border-blue-300/60 hover:shadow-sm border border-sidebar-border/40 bg-sidebar/30",
|
||||
isActive &&
|
||||
"bg-blue-100 border-blue-400 shadow-md ring-1 ring-blue-200/50 hover:bg-blue-100 hover:border-blue-400",
|
||||
)}
|
||||
>
|
||||
<div className="space-y-1.5">
|
||||
<h3 className="text-sm font-medium line-clamp-2 leading-tight text-sidebar-foreground">
|
||||
{title}
|
||||
</h3>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-1 text-xs text-sidebar-foreground/70">
|
||||
<MessageSquareIcon className="w-3 h-3" />
|
||||
<span>{session.meta.messageCount}</span>
|
||||
</div>
|
||||
{session.meta.lastModifiedAt && (
|
||||
<span className="text-xs text-sidebar-foreground/60">
|
||||
{new Date(session.meta.lastModifiedAt).toLocaleDateString(
|
||||
"en-US",
|
||||
{
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
},
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
export const SessionSidebar: FC<{
|
||||
currentSessionId: string;
|
||||
projectId: string;
|
||||
className?: string;
|
||||
}> = ({ currentSessionId, projectId, className }) => {
|
||||
const {
|
||||
data: { sessions },
|
||||
} = useProject(projectId);
|
||||
const [isCollapsed, setIsCollapsed] = useState(false);
|
||||
|
||||
return (
|
||||
<Collapsible
|
||||
open={!isCollapsed}
|
||||
onOpenChange={(open) => setIsCollapsed(!open)}
|
||||
className={cn("hidden md:flex h-full", className)}
|
||||
>
|
||||
<div className="relative h-full">
|
||||
<div
|
||||
className={cn(
|
||||
"h-full border-r border-sidebar-border transition-all duration-300 ease-in-out",
|
||||
isCollapsed ? "w-12" : "w-72 lg:w-80",
|
||||
)}
|
||||
>
|
||||
<CollapsibleTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="absolute -right-3 top-4 z-10 bg-background border border-sidebar-border shadow-sm"
|
||||
>
|
||||
<PanelLeftIcon
|
||||
className={cn(
|
||||
"w-4 h-4 transition-transform duration-200",
|
||||
isCollapsed && "rotate-180",
|
||||
)}
|
||||
/>
|
||||
</Button>
|
||||
</CollapsibleTrigger>
|
||||
|
||||
<CollapsibleContent className="h-full data-[state=closed]:animate-slide-out-to-left data-[state=open]:animate-slide-in-from-left">
|
||||
<SidebarContent
|
||||
sessions={sessions}
|
||||
currentSessionId={currentSessionId}
|
||||
projectId={projectId}
|
||||
/>
|
||||
</CollapsibleContent>
|
||||
|
||||
{isCollapsed && (
|
||||
<div className="h-full bg-sidebar border-r border-sidebar-border flex flex-col items-center pt-16">
|
||||
<MessageSquareIcon className="w-5 h-5 text-sidebar-foreground/70" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Collapsible>
|
||||
);
|
||||
};
|
||||
@@ -96,7 +96,7 @@ export const getSessionMeta = async (
|
||||
messageCount: lines.length,
|
||||
firstCommand: getFirstCommand(jsonlFilePath, lines),
|
||||
lastModifiedAt: lastModifiedUnixTime
|
||||
? new Date(lastModifiedUnixTime)
|
||||
? new Date(lastModifiedUnixTime).toISOString()
|
||||
: null,
|
||||
};
|
||||
|
||||
|
||||
@@ -5,6 +5,11 @@ import { decodeProjectId } from "../project/id";
|
||||
import type { Session } from "../types";
|
||||
import { getSessionMeta } from "./getSessionMeta";
|
||||
|
||||
const getTime = (date: string | null) => {
|
||||
if (date === null) return 0;
|
||||
return new Date(date).getTime();
|
||||
};
|
||||
|
||||
export const getSessions = async (
|
||||
projectId: string,
|
||||
): Promise<{ sessions: Session[] }> => {
|
||||
@@ -27,10 +32,7 @@ export const getSessions = async (
|
||||
|
||||
return {
|
||||
sessions: sessions.sort((a, b) => {
|
||||
return (
|
||||
(b.meta.lastModifiedAt?.getTime() ?? 0) -
|
||||
(a.meta.lastModifiedAt?.getTime() ?? 0)
|
||||
);
|
||||
return getTime(b.meta.lastModifiedAt) - getTime(a.meta.lastModifiedAt);
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -23,7 +23,7 @@ export type Session = {
|
||||
export type SessionMeta = {
|
||||
messageCount: number;
|
||||
firstCommand: ParsedCommand | null;
|
||||
lastModifiedAt: Date | null;
|
||||
lastModifiedAt: string | null;
|
||||
};
|
||||
|
||||
export type SessionDetail = Session & {
|
||||
|
||||
Reference in New Issue
Block a user