feat(ui): VersionHistorySidePanel antd Drawer \u2192 shadcn Sheet

- antd Drawer/List/Skeleton/Tag/Typography \u2192 shadcn Sheet/Badge/
  Skeleton + plain span.
- modal={false} + onInteractOutside={e.preventDefault()} on the
  SheetContent preserves the existing 'interact with the main editor
  while drawer is open' behaviour from antd Drawer's mask={false}.
- Blue Latest / emerald Active badges are categorical and added to
  .eslintrc.json overrides.

Gates: TS \u2713 | Build \u2713.

Co-authored-by: yuneng-jiang <yuneng-berri@users.noreply.github.com>
This commit is contained in:
Cursor Agent 2026-04-23 12:49:32 +00:00
parent 8a052c1ea6
commit 7b725e498f
No known key found for this signature in database
2 changed files with 114 additions and 84 deletions

View file

@ -59,6 +59,7 @@
"src/components/guardrails/content_filter/PatternTable.tsx",
"src/components/guardrails/content_filter/ContentFilterDisplay.tsx",
"src/components/prompts/prompt_editor_view/PromptEditorHeader.tsx",
"src/components/prompts/prompt_editor_view/VersionHistorySidePanel.tsx",
"src/app/(dashboard)/teams/components/TeamsTable/ModelsCell.tsx",
"src/components/prompts/prompt_editor_view/conversation_panel/MessageBubble.tsx",
"src/components/playground/chat_ui/SessionManagement.tsx",

View file

@ -1,9 +1,15 @@
import { Drawer, List, Skeleton, Tag, Typography } from "antd";
import {
Sheet,
SheetContent,
SheetHeader,
SheetTitle,
} from "@/components/ui/sheet";
import { Badge } from "@/components/ui/badge";
import { Skeleton } from "@/components/ui/skeleton";
import { cn } from "@/lib/utils";
import React, { useEffect, useState } from "react";
import { getPromptVersions, PromptSpec } from "../../networking";
const { Text } = Typography;
interface VersionHistorySidePanelProps {
isOpen: boolean;
onClose: () => void;
@ -25,33 +31,34 @@ const VersionHistorySidePanel: React.FC<VersionHistorySidePanelProps> = ({
const [loading, setLoading] = useState(false);
useEffect(() => {
const fetchVersions = async () => {
setLoading(true);
try {
const basePromptId = promptId.includes(".v")
? promptId.split(".v")[0]
: promptId;
const response = await getPromptVersions(accessToken!, basePromptId);
setVersions(response.prompts);
} catch (error) {
console.error("Error fetching prompt versions:", error);
} finally {
setLoading(false);
}
};
if (isOpen && accessToken && promptId) {
fetchVersions();
}
}, [isOpen, accessToken, promptId]);
const fetchVersions = async () => {
setLoading(true);
try {
// Strip .v suffix if present to get base ID for querying all versions
const basePromptId = promptId.includes(".v") ? promptId.split(".v")[0] : promptId;
const response = await getPromptVersions(accessToken!, basePromptId);
setVersions(response.prompts);
} catch (error) {
console.error("Error fetching prompt versions:", error);
} finally {
setLoading(false);
}
};
const getVersionNumber = (prompt: PromptSpec) => {
// Use explicit version field if available, otherwise try to extract from litellm_params.prompt_id
if (prompt.version) {
return `v${prompt.version}`;
}
// Fallback: try to extract from litellm_params.prompt_id
const versionedId = (prompt.litellm_params as any)?.prompt_id || prompt.prompt_id;
const versionedId =
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(prompt.litellm_params as any)?.prompt_id || prompt.prompt_id;
if (versionedId.includes(".v")) {
return `v${versionedId.split(".v")[1]}`;
}
@ -67,73 +74,95 @@ const VersionHistorySidePanel: React.FC<VersionHistorySidePanelProps> = ({
};
return (
<Drawer
title="Version History"
placement="right"
onClose={onClose}
<Sheet
open={isOpen}
width={400}
mask={false} // Allow interacting with the main editor while drawer is open
maskClosable={false}
onOpenChange={(o) => (!o ? onClose() : undefined)}
modal={false}
>
{loading ? (
<Skeleton active paragraph={{ rows: 4 }} />
) : versions.length === 0 ? (
<div className="text-center py-8 text-gray-500">No version history available.</div>
) : (
<List
dataSource={versions}
renderItem={(item, index) => {
// Use version field for comparison since all items have the same prompt_id
const itemVersionNum = item.version || parseInt(getVersionNumber(item).replace('v', ''));
// Extract version number from activeVersionId (may have .vX suffix)
let activeVersionNum: number | null = null;
if (activeVersionId) {
if (activeVersionId.includes('.v')) {
activeVersionNum = parseInt(activeVersionId.split('.v')[1]);
} else if (activeVersionId.includes('_v')) {
activeVersionNum = parseInt(activeVersionId.split('_v')[1]);
}
}
// Default to latest (first item) if no activeVersionId
const isSelected = activeVersionNum ? itemVersionNum === activeVersionNum : index === 0;
return (
<div
key={`${item.prompt_id}-v${item.version || itemVersionNum}`}
className={`mb-4 p-4 rounded-lg border cursor-pointer transition-all hover:shadow-md ${
isSelected ? "border-blue-500 bg-blue-50" : "border-gray-200 bg-white hover:border-blue-300"
}`}
onClick={() => onSelectVersion?.(item)}
>
<div className="flex justify-between items-start mb-2">
<div className="flex items-center gap-2">
<Tag className="m-0">
{getVersionNumber(item)}
</Tag>
{index === 0 && <Tag color="blue" className="m-0">Latest</Tag>}
</div>
{isSelected && (
<Tag color="green" className="m-0">
Active
</Tag>
)}
</div>
<SheetContent
side="right"
className="w-[400px] sm:max-w-[400px]"
onInteractOutside={(e) => e.preventDefault()}
>
<SheetHeader>
<SheetTitle>Version History</SheetTitle>
</SheetHeader>
{loading ? (
<div className="space-y-3 mt-4">
{[1, 2, 3, 4].map((i) => (
<Skeleton key={i} className="h-16 w-full" />
))}
</div>
) : versions.length === 0 ? (
<div className="text-center py-8 text-muted-foreground">
No version history available.
</div>
) : (
<div className="mt-4 overflow-y-auto">
{versions.map((item, index) => {
const itemVersionNum =
item.version ||
parseInt(getVersionNumber(item).replace("v", ""));
<div className="flex flex-col gap-1">
<Text className="text-sm text-gray-600 font-medium">{formatDate(item.created_at)}</Text>
<Text type="secondary" className="text-xs">
{item.prompt_info?.prompt_type === "db" ? "Saved to Database" : "Config Prompt"}
</Text>
let activeVersionNum: number | null = null;
if (activeVersionId) {
if (activeVersionId.includes(".v")) {
activeVersionNum = parseInt(activeVersionId.split(".v")[1]);
} else if (activeVersionId.includes("_v")) {
activeVersionNum = parseInt(activeVersionId.split("_v")[1]);
}
}
const isSelected = activeVersionNum
? itemVersionNum === activeVersionNum
: index === 0;
return (
<div
key={`${item.prompt_id}-v${item.version || itemVersionNum}`}
className={cn(
"mb-4 p-4 rounded-lg border cursor-pointer transition-all hover:shadow-md",
isSelected
? "border-primary bg-primary/10"
: "border-border bg-background hover:border-primary/50",
)}
onClick={() => onSelectVersion?.(item)}
>
<div className="flex justify-between items-start mb-2">
<div className="flex items-center gap-2">
<Badge variant="outline" className="m-0">
{getVersionNumber(item)}
</Badge>
{index === 0 && (
<Badge className="m-0 bg-blue-100 text-blue-700 dark:bg-blue-950 dark:text-blue-300">
Latest
</Badge>
)}
</div>
{isSelected && (
<Badge className="m-0 bg-emerald-100 text-emerald-700 dark:bg-emerald-950 dark:text-emerald-300">
Active
</Badge>
)}
</div>
<div className="flex flex-col gap-1">
<span className="text-sm text-muted-foreground font-medium">
{formatDate(item.created_at)}
</span>
<span className="text-xs text-muted-foreground">
{item.prompt_info?.prompt_type === "db"
? "Saved to Database"
: "Config Prompt"}
</span>
</div>
</div>
</div>
);
}}
/>
)}
</Drawer>
);
})}
</div>
)}
</SheetContent>
</Sheet>
);
};