Files
CC-Switch/src/components/prompts/PromptPanel.tsx
T
Jason b8305f281b fix(ui): align panel content with header constraints
Add max-w-[56rem] and mx-auto to Skills, MCP, and Prompts panels
to match the header layout constraints, ensuring the back button
and action buttons align with the content area.
2026-01-22 12:41:06 +08:00

169 lines
5.1 KiB
TypeScript

import React, { useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { FileText } from "lucide-react";
import { type AppId } from "@/lib/api";
import { usePromptActions } from "@/hooks/usePromptActions";
import PromptListItem from "./PromptListItem";
import PromptFormPanel from "./PromptFormPanel";
import { ConfirmDialog } from "../ConfirmDialog";
interface PromptPanelProps {
open: boolean;
onOpenChange: (open: boolean) => void;
appId: AppId;
}
export interface PromptPanelHandle {
openAdd: () => void;
}
const PromptPanel = React.forwardRef<PromptPanelHandle, PromptPanelProps>(
({ open, appId }, ref) => {
const { t } = useTranslation();
const [isFormOpen, setIsFormOpen] = useState(false);
const [editingId, setEditingId] = useState<string | null>(null);
const [confirmDialog, setConfirmDialog] = useState<{
isOpen: boolean;
titleKey: string;
messageKey: string;
messageParams?: Record<string, unknown>;
onConfirm: () => void;
} | null>(null);
const {
prompts,
loading,
reload,
savePrompt,
deletePrompt,
toggleEnabled,
} = usePromptActions(appId);
useEffect(() => {
if (open) reload();
}, [open, reload]);
// Listen for prompt import events from deep link
useEffect(() => {
const handlePromptImported = (event: Event) => {
const customEvent = event as CustomEvent;
// Reload if the import is for this app
if (customEvent.detail?.app === appId) {
reload();
}
};
window.addEventListener("prompt-imported", handlePromptImported);
return () => {
window.removeEventListener("prompt-imported", handlePromptImported);
};
}, [appId, reload]);
const handleAdd = () => {
setEditingId(null);
setIsFormOpen(true);
};
React.useImperativeHandle(ref, () => ({
openAdd: handleAdd,
}));
const handleEdit = (id: string) => {
setEditingId(id);
setIsFormOpen(true);
};
const handleDelete = (id: string) => {
const prompt = prompts[id];
setConfirmDialog({
isOpen: true,
titleKey: "prompts.confirm.deleteTitle",
messageKey: "prompts.confirm.deleteMessage",
messageParams: { name: prompt?.name },
onConfirm: async () => {
try {
await deletePrompt(id);
setConfirmDialog(null);
} catch (e) {
// Error handled by hook
}
},
});
};
const promptEntries = useMemo(() => Object.entries(prompts), [prompts]);
const enabledPrompt = promptEntries.find(([_, p]) => p.enabled);
return (
<div className="flex flex-col h-[calc(100vh-8rem)] px-4 max-w-[56rem] mx-auto">
<div className="flex-shrink-0 py-4 glass rounded-xl border border-white/10 mb-4 px-6">
<div className="text-sm text-muted-foreground">
{t("prompts.count", { count: promptEntries.length })} ·{" "}
{enabledPrompt
? t("prompts.enabledName", { name: enabledPrompt[1].name })
: t("prompts.noneEnabled")}
</div>
</div>
<div className="flex-1 overflow-y-auto pb-16">
{loading ? (
<div className="text-center py-12 text-muted-foreground">
{t("prompts.loading")}
</div>
) : promptEntries.length === 0 ? (
<div className="text-center py-12">
<div className="w-16 h-16 mx-auto mb-4 bg-muted rounded-full flex items-center justify-center">
<FileText size={24} className="text-muted-foreground" />
</div>
<h3 className="text-lg font-medium text-foreground mb-2">
{t("prompts.empty")}
</h3>
<p className="text-muted-foreground text-sm">
{t("prompts.emptyDescription")}
</p>
</div>
) : (
<div className="space-y-3">
{promptEntries.map(([id, prompt]) => (
<PromptListItem
key={id}
id={id}
prompt={prompt}
onToggle={toggleEnabled}
onEdit={handleEdit}
onDelete={handleDelete}
/>
))}
</div>
)}
</div>
{isFormOpen && (
<PromptFormPanel
appId={appId}
editingId={editingId || undefined}
initialData={editingId ? prompts[editingId] : undefined}
onSave={savePrompt}
onClose={() => setIsFormOpen(false)}
/>
)}
{confirmDialog && (
<ConfirmDialog
isOpen={confirmDialog.isOpen}
title={t(confirmDialog.titleKey)}
message={t(confirmDialog.messageKey, confirmDialog.messageParams)}
onConfirm={confirmDialog.onConfirm}
onCancel={() => setConfirmDialog(null)}
/>
)}
</div>
);
},
);
PromptPanel.displayName = "PromptPanel";
export default PromptPanel;