Merge pull request #25 from heimoshuiyu/cursor

feat: Add edit and delete functionality for templates and enhance JSON editor
This commit is contained in:
2025-05-28 09:58:50 +08:00
committed by GitHub
5 changed files with 6111 additions and 47 deletions

View File

@@ -11,6 +11,7 @@
"dependencies": { "dependencies": {
"@heroicons/react": "^2.2.0", "@heroicons/react": "^2.2.0",
"@hookform/resolvers": "^3.9.1", "@hookform/resolvers": "^3.9.1",
"@monaco-editor/react": "^4.7.0",
"@radix-ui/react-accordion": "^1.2.2", "@radix-ui/react-accordion": "^1.2.2",
"@radix-ui/react-alert-dialog": "^1.1.4", "@radix-ui/react-alert-dialog": "^1.1.4",
"@radix-ui/react-aspect-ratio": "^1.1.1", "@radix-ui/react-aspect-ratio": "^1.1.1",

View File

@@ -1,4 +1,4 @@
import React, { useContext } from "react"; import React, { useContext, useState, useRef } from "react";
import { import {
ChatStore, ChatStore,
TemplateAPI, TemplateAPI,
@@ -6,6 +6,7 @@ import {
TemplateTools, TemplateTools,
} from "@/types/chatstore"; } from "@/types/chatstore";
import { Tr } from "@/translate"; import { Tr } from "@/translate";
import Editor from "@monaco-editor/react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
@@ -35,14 +36,17 @@ import {
Dialog, Dialog,
DialogContent, DialogContent,
DialogHeader, DialogHeader,
DialogTrigger, DialogTitle,
DialogFooter,
DialogClose,
} from "./ui/dialog"; } from "./ui/dialog";
import { DialogTitle } from "@radix-ui/react-dialog";
import { Textarea } from "./ui/textarea"; import { Textarea } from "./ui/textarea";
import { Label } from "./ui/label"; import { Label } from "./ui/label";
import { Input } from "./ui/input"; import { Input } from "./ui/input";
import { SetAPIsTemplate } from "./setAPIsTemplate"; import { SetAPIsTemplate } from "./setAPIsTemplate";
import { isVailedJSON } from "@/utils/isVailedJSON"; import { isVailedJSON } from "@/utils/isVailedJSON";
import { toast } from 'sonner';
import { ConfirmationDialog } from "./ui/confirmation-dialog";
interface APITemplateDropdownProps { interface APITemplateDropdownProps {
label: string; label: string;
@@ -50,6 +54,81 @@ interface APITemplateDropdownProps {
apiField: string; apiField: string;
keyField: string; keyField: string;
} }
interface EditTemplateDialogProps {
template: TemplateAPI;
onSave: (updatedTemplate: TemplateAPI) => void;
onClose: () => void;
}
function EditTemplateDialog({ template, onSave, onClose }: EditTemplateDialogProps) {
const [name, setName] = useState(template.name);
const [endpoint, setEndpoint] = useState(template.endpoint);
const [key, setKey] = useState(template.key);
const { toast } = useToast();
const handleSave = () => {
if (!name.trim()) {
toast({
title: "Error",
description: "Template name cannot be empty",
variant: "destructive",
});
return;
}
onSave({
...template,
name: name.trim(),
endpoint: endpoint.trim(),
key: key.trim(),
});
onClose();
};
return (
<Dialog open onOpenChange={onClose}>
<DialogContent>
<DialogHeader>
<DialogTitle>Edit Template</DialogTitle>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid gap-2">
<Label htmlFor="name">Template Name</Label>
<Input
id="name"
value={name}
onChange={(e) => setName(e.target.value)}
/>
</div>
<div className="grid gap-2">
<Label htmlFor="endpoint">API Endpoint</Label>
<Input
id="endpoint"
value={endpoint}
onChange={(e) => setEndpoint(e.target.value)}
/>
</div>
<div className="grid gap-2">
<Label htmlFor="key">API Key</Label>
<Input
id="key"
value={key}
onChange={(e) => setKey(e.target.value)}
/>
</div>
</div>
<DialogFooter>
<DialogClose asChild>
<Button variant="outline">Cancel</Button>
</DialogClose>
<Button onClick={handleSave}>Save Changes</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
function APIsDropdownList({ function APIsDropdownList({
label, label,
shortLabel, shortLabel,
@@ -70,18 +149,60 @@ function APIsDropdownList({
setTemplateAPIsWhisper, setTemplateAPIsWhisper,
setTemplateTools, setTemplateTools,
} = useContext(AppContext); } = useContext(AppContext);
const { toast } = useToast();
const [open, setOpen] = React.useState(false);
const [editingTemplate, setEditingTemplate] = useState<TemplateAPI | null>(null);
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [templateToDelete, setTemplateToDelete] = useState<TemplateAPI | null>(null);
let API = templateAPIs; let API = templateAPIs;
let setAPI = setTemplateAPIs;
if (label === "Chat API") { if (label === "Chat API") {
API = templateAPIs; API = templateAPIs;
setAPI = setTemplateAPIs;
} else if (label === "Whisper API") { } else if (label === "Whisper API") {
API = templateAPIsWhisper; API = templateAPIsWhisper;
setAPI = setTemplateAPIsWhisper;
} else if (label === "TTS API") { } else if (label === "TTS API") {
API = templateAPIsTTS; API = templateAPIsTTS;
setAPI = setTemplateAPIsTTS;
} else if (label === "Image Gen API") { } else if (label === "Image Gen API") {
API = templateAPIsImageGen; API = templateAPIsImageGen;
setAPI = setTemplateAPIsImageGen;
} }
const [open, setOpen] = React.useState(false); const handleEdit = (template: TemplateAPI) => {
setEditingTemplate(template);
};
const handleSave = (updatedTemplate: TemplateAPI) => {
const index = API.findIndex(t => t.name === updatedTemplate.name);
if (index !== -1) {
const newAPI = [...API];
newAPI[index] = updatedTemplate;
setAPI(newAPI);
toast({
title: "Success",
description: "Template updated successfully",
});
}
};
const handleDelete = (template: TemplateAPI) => {
setTemplateToDelete(template);
setDeleteDialogOpen(true);
};
const confirmDelete = () => {
if (templateToDelete) {
const newAPI = API.filter(t => t.name !== templateToDelete.name);
setAPI(newAPI);
toast({
title: "Success",
description: "Template deleted successfully",
});
}
};
return ( return (
<div className="flex items-center space-x-4 mx-3"> <div className="flex items-center space-x-4 mx-3">
@@ -116,10 +237,34 @@ function APIsDropdownList({
[apiField]: t.endpoint, [apiField]: t.endpoint,
[keyField]: t.key, [keyField]: t.key,
}); });
setOpen(false); // Close popover after selecting setOpen(false);
}} }}
> >
{t.name} <div className="flex items-center justify-between w-full">
<span>{t.name}</span>
<div className="flex gap-2">
<Button
variant="ghost"
size="icon"
onClick={(e) => {
e.stopPropagation();
handleEdit(t);
}}
>
<EditIcon className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="icon"
onClick={(e) => {
e.stopPropagation();
handleDelete(t);
}}
>
<DeleteIcon className="h-4 w-4" />
</Button>
</div>
</div>
</CommandItem> </CommandItem>
))} ))}
</CommandGroup> </CommandGroup>
@@ -127,6 +272,23 @@ function APIsDropdownList({
</Command> </Command>
</PopoverContent> </PopoverContent>
</Popover> </Popover>
{editingTemplate && (
<EditTemplateDialog
template={editingTemplate}
onSave={handleSave}
onClose={() => setEditingTemplate(null)}
/>
)}
<ConfirmationDialog
isOpen={deleteDialogOpen}
onClose={() => {
setDeleteDialogOpen(false);
setTemplateToDelete(null);
}}
onConfirm={confirmDelete}
title="Delete Template"
description={`Are you sure you want to delete "${templateToDelete?.name}"?`}
/>
</div> </div>
); );
} }
@@ -207,12 +369,161 @@ function ToolsDropdownList() {
); );
} }
interface EditChatTemplateDialogProps {
template: TemplateChatStore;
onSave: (updatedTemplate: TemplateChatStore) => void;
onClose: () => void;
}
function EditChatTemplateDialog({ template, onSave, onClose }: EditChatTemplateDialogProps) {
const [name, setName] = useState(template.name);
const [jsonContent, setJsonContent] = useState(() => {
const { name: _, ...rest } = template;
return JSON.stringify(rest, null, 2);
});
const [editor, setEditor] = useState<any>(null);
const handleEditorDidMount = (editor: any) => {
setEditor(editor);
};
const handleFormat = () => {
if (editor) {
editor.getAction('editor.action.formatDocument').run();
}
};
const handleSave = () => {
if (!name.trim()) {
toast.error('Template name cannot be empty');
return;
}
try {
const parsedJson = JSON.parse(jsonContent);
const updatedTemplate: TemplateChatStore = {
name: name.trim(),
...parsedJson
};
onSave(updatedTemplate);
toast.success('Template updated successfully');
} catch (error) {
toast.error('Invalid JSON format');
}
};
return (
<Dialog open onOpenChange={onClose}>
<DialogContent className="max-w-4xl">
<DialogHeader>
<DialogTitle>Edit Template</DialogTitle>
</DialogHeader>
<div className="space-y-4">
<div>
<Label htmlFor="name">Template Name</Label>
<Input
id="name"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Enter template name"
/>
</div>
<div>
<Label>Template Content (JSON)</Label>
<div className="relative">
<Button
variant="outline"
size="sm"
className="absolute right-2 top-2 z-10"
onClick={handleFormat}
>
Format JSON
</Button>
<div className="h-[400px] border rounded-md">
<Editor
height="400px"
defaultLanguage="json"
value={jsonContent}
onChange={(value) => setJsonContent(value || '')}
onMount={handleEditorDidMount}
options={{
minimap: { enabled: false },
fontSize: 14,
lineNumbers: 'on',
scrollBeyondLastLine: false,
automaticLayout: true,
tabSize: 2,
wordWrap: 'on'
}}
/>
</div>
</div>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={onClose}>
Cancel
</Button>
<Button onClick={handleSave}>Save Changes</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
function ChatTemplateDropdownList() { function ChatTemplateDropdownList() {
const ctx = useContext(AppContext); const ctx = useContext(AppContext);
const { chatStore, setChatStore } = useContext(AppChatStoreContext); const { chatStore, setChatStore } = useContext(AppChatStoreContext);
const { templates, setTemplates } = useContext(AppContext); const { templates, setTemplates } = useContext(AppContext);
const [open, setOpen] = React.useState(false); const [open, setOpen] = React.useState(false);
const [editingTemplate, setEditingTemplate] = useState<TemplateChatStore | null>(null);
const { toast } = useToast();
const [confirmDialogOpen, setConfirmDialogOpen] = useState(false);
const [templateToApply, setTemplateToApply] = useState<TemplateChatStore | null>(null);
const handleEdit = (template: TemplateChatStore) => {
setEditingTemplate(template);
};
const handleSave = (updatedTemplate: TemplateChatStore) => {
const index = templates.findIndex(t => t.name === updatedTemplate.name);
if (index !== -1) {
const newTemplates = [...templates];
newTemplates[index] = updatedTemplate;
setTemplates(newTemplates);
toast({
title: "Success",
description: "Template updated successfully",
});
}
};
const handleDelete = (template: TemplateChatStore) => {
setTemplateToApply(template);
setConfirmDialogOpen(true);
};
const handleTemplateSelect = (template: TemplateChatStore) => {
if (chatStore.history.length > 0 || chatStore.systemMessageContent) {
setTemplateToApply(template);
setConfirmDialogOpen(true);
} else {
applyTemplate(template);
}
};
const applyTemplate = (template: TemplateChatStore) => {
setChatStore({
...newChatStore({
...chatStore,
...{
use_this_history: template.history ?? chatStore.history,
},
...template,
}),
});
setOpen(false);
};
return ( return (
<div className="flex items-center space-x-4 mx-3"> <div className="flex items-center space-x-4 mx-3">
@@ -237,34 +548,33 @@ function ChatTemplateDropdownList() {
<CommandItem <CommandItem
key={index} key={index}
value={t.name} value={t.name}
onSelect={() => { onSelect={() => handleTemplateSelect(t)}
// Update chatStore with the selected template
if (
chatStore.history.length > 0 ||
chatStore.systemMessageContent
) {
console.log("you clicked", t.name);
const confirm = window.confirm(
"This will replace the current chat history. Are you sure? "
);
if (!confirm) {
setOpen(false); // Close popover even if not confirmed
return;
}
}
setChatStore({
...newChatStore({
...chatStore,
...{
use_this_history: t.history ?? chatStore.history,
},
...t,
}),
});
setOpen(false); // Close popover after selecting
}}
> >
{t.name} <div className="flex items-center justify-between w-full">
<span>{t.name}</span>
<div className="flex gap-2">
<Button
variant="ghost"
size="icon"
onClick={(e) => {
e.stopPropagation();
handleEdit(t);
}}
>
<EditIcon className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="icon"
onClick={(e) => {
e.stopPropagation();
handleDelete(t);
}}
>
<DeleteIcon className="h-4 w-4" />
</Button>
</div>
</div>
</CommandItem> </CommandItem>
))} ))}
</CommandGroup> </CommandGroup>
@@ -272,6 +582,23 @@ function ChatTemplateDropdownList() {
</Command> </Command>
</PopoverContent> </PopoverContent>
</Popover> </Popover>
{editingTemplate && (
<EditChatTemplateDialog
template={editingTemplate}
onSave={handleSave}
onClose={() => setEditingTemplate(null)}
/>
)}
<ConfirmationDialog
isOpen={confirmDialogOpen}
onClose={() => {
setConfirmDialogOpen(false);
setTemplateToApply(null);
}}
onConfirm={() => templateToApply && applyTemplate(templateToApply)}
title="Replace Chat History"
description="This will replace the current chat history. Are you sure?"
/>
</div> </div>
); );
} }

View File

@@ -14,6 +14,7 @@ import {
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import { Button } from "./ui/button"; import { Button } from "./ui/button";
import { AppChatStoreContext, AppContext } from "../pages/App"; import { AppChatStoreContext, AppContext } from "../pages/App";
import { ConfirmationDialog } from "./ui/confirmation-dialog";
interface EditMessageProps { interface EditMessageProps {
chat: ChatStoreMessage; chat: ChatStoreMessage;
@@ -22,9 +23,19 @@ interface EditMessageProps {
} }
export function EditMessage(props: EditMessageProps) { export function EditMessage(props: EditMessageProps) {
const { chatStore, setChatStore } = useContext(AppChatStoreContext); const { chatStore, setChatStore } = useContext(AppChatStoreContext);
const [showConfirmDialog, setShowConfirmDialog] = useState(false);
const { showEdit, setShowEdit, chat } = props; const { showEdit, setShowEdit, chat } = props;
const handleSwitchMessageType = () => {
if (typeof chat.content === "string") {
chat.content = [];
} else {
chat.content = "";
}
setChatStore({ ...chatStore });
};
return ( return (
<Dialog open={showEdit} onOpenChange={setShowEdit}> <Dialog open={showEdit} onOpenChange={setShowEdit}>
{/* <DialogTrigger> {/* <DialogTrigger>
@@ -46,19 +57,7 @@ export function EditMessage(props: EditMessageProps) {
<Button <Button
variant="destructive" variant="destructive"
className="w-full" className="w-full"
onClick={() => { onClick={() => setShowConfirmDialog(true)}
const confirm = window.confirm(
"Change message type will clear the content, are you sure?"
);
if (!confirm) return;
if (typeof chat.content === "string") {
chat.content = [];
} else {
chat.content = "";
}
setChatStore({ ...chatStore });
}}
> >
Switch to{" "} Switch to{" "}
{typeof chat.content === "string" {typeof chat.content === "string"
@@ -68,6 +67,13 @@ export function EditMessage(props: EditMessageProps) {
)} )}
<Button onClick={() => setShowEdit(false)}>Close</Button> <Button onClick={() => setShowEdit(false)}>Close</Button>
</DialogContent> </DialogContent>
<ConfirmationDialog
isOpen={showConfirmDialog}
onClose={() => setShowConfirmDialog(false)}
onConfirm={handleSwitchMessageType}
title="Switch Message Type"
description="Change message type will clear the content, are you sure?"
/>
</Dialog> </Dialog>
); );
} }

View File

@@ -0,0 +1,48 @@
import React from "react";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter,
DialogDescription,
} from "./dialog";
import { Button } from "./button";
interface ConfirmationDialogProps {
isOpen: boolean;
onClose: () => void;
onConfirm: () => void;
title: string;
description: string;
}
export function ConfirmationDialog({
isOpen,
onClose,
onConfirm,
title,
description
}: ConfirmationDialogProps) {
return (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent>
<DialogHeader>
<DialogTitle>{title}</DialogTitle>
<DialogDescription>{description}</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="outline" onClick={onClose}>
Cancel
</Button>
<Button variant="destructive" onClick={() => {
onConfirm();
onClose();
}}>
Confirm
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

5682
yarn.lock Normal file

File diff suppressed because it is too large Load Diff