mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-25 13:45:03 +08:00
a1537807eb
* feat(failover): add auto-failover master switch with proxy integration
- Add persistent auto_failover_enabled setting in database
- Add get/set_auto_failover_enabled commands
- Provider router respects master switch state
- Proxy shutdown automatically disables failover
- Enabling failover auto-starts proxy server
- Optimistic updates for failover queue toggle
* feat(proxy): persist proxy takeover state across app restarts
- Add proxy_takeover_{app_type} settings for per-app state tracking
- Restore proxy takeover state automatically on app startup
- Preserve state on normal exit, clear on manual stop
- Add stop_with_restore_keep_state method for graceful shutdown
* fix(proxy): set takeover state for all apps in start_with_takeover
169 lines
5.1 KiB
TypeScript
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="mx-auto max-w-[56rem] flex flex-col h-[calc(100vh-8rem)] px-6">
|
|
<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;
|