Fix: Template attribute dialog input and type issues
This commit is contained in:
@@ -77,6 +77,7 @@ import { Slider } from "@/components/ui/slider";
|
|||||||
import { NonOverflowScrollArea, ScrollArea } from "@/components/ui/scroll-area";
|
import { NonOverflowScrollArea, ScrollArea } from "@/components/ui/scroll-area";
|
||||||
import { AppChatStoreContext, AppContext } from "@/pages/App";
|
import { AppChatStoreContext, AppContext } from "@/pages/App";
|
||||||
import { toast } from "@/hooks/use-toast";
|
import { toast } from "@/hooks/use-toast";
|
||||||
|
import { TemplateAttributeDialog } from "@/components/TemplateAttributeDialog";
|
||||||
|
|
||||||
const TTS_VOICES: string[] = [
|
const TTS_VOICES: string[] = [
|
||||||
"alloy",
|
"alloy",
|
||||||
@@ -739,6 +740,7 @@ export default (props: {}) => {
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const { langCode, setLangCode } = useContext(langCodeContext);
|
const { langCode, setLangCode } = useContext(langCodeContext);
|
||||||
const [open, setOpen] = useState<boolean>(false);
|
const [open, setOpen] = useState<boolean>(false);
|
||||||
|
const [showTemplateDialog, setShowTemplateDialog] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
themeChange(false);
|
themeChange(false);
|
||||||
@@ -804,7 +806,7 @@ export default (props: {}) => {
|
|||||||
<LongInput
|
<LongInput
|
||||||
label="System Prompt"
|
label="System Prompt"
|
||||||
field="systemMessageContent"
|
field="systemMessageContent"
|
||||||
help="系统消息,用于指示ChatGPT的角色和一些前置条件,例如“你是一个有帮助的人工智能助理”,或者“你是一个专业英语翻译,把我的话全部翻译成英语”,详情参考 OPEAN AI API 文档"
|
help="System prompt, used to indicate the role of ChatGPT and some preconditions, such as 'You are a helpful AI assistant' or 'You are a professional English translator, translate my words into English', please refer to the OpenAI API documentation"
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -1051,21 +1053,7 @@ export default (props: {}) => {
|
|||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="w-full"
|
className="w-full"
|
||||||
onClick={() => {
|
onClick={() => setShowTemplateDialog(true)}
|
||||||
const name = prompt(
|
|
||||||
tr("Give this template a name:", langCode)
|
|
||||||
);
|
|
||||||
if (!name) {
|
|
||||||
alert(tr("No template name specified", langCode));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const tmp: ChatStore = structuredClone(chatStore);
|
|
||||||
tmp.history = tmp.history.filter((h) => h.example);
|
|
||||||
// @ts-ignore
|
|
||||||
tmp.name = name;
|
|
||||||
templates.push(tmp as TemplateChatStore);
|
|
||||||
setTemplates([...templates]);
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<Tr>As template</Tr>
|
<Tr>As template</Tr>
|
||||||
</Button>
|
</Button>
|
||||||
@@ -1594,6 +1582,25 @@ export default (props: {}) => {
|
|||||||
</div>
|
</div>
|
||||||
</NonOverflowScrollArea>
|
</NonOverflowScrollArea>
|
||||||
</SheetContent>
|
</SheetContent>
|
||||||
|
|
||||||
|
<TemplateAttributeDialog
|
||||||
|
open={showTemplateDialog}
|
||||||
|
chatStore={chatStore}
|
||||||
|
langCode={langCode}
|
||||||
|
onClose={() => setShowTemplateDialog(false)}
|
||||||
|
onSave={(name, selectedAttributes) => {
|
||||||
|
const tmp: ChatStore = {
|
||||||
|
...chatStore,
|
||||||
|
...selectedAttributes,
|
||||||
|
history: chatStore.history.filter((h) => h.example),
|
||||||
|
};
|
||||||
|
// @ts-ignore
|
||||||
|
tmp.name = name;
|
||||||
|
templates.push(tmp as TemplateChatStore);
|
||||||
|
setTemplates([...templates]);
|
||||||
|
setShowTemplateDialog(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</Sheet>
|
</Sheet>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
142
src/components/TemplateAttributeDialog.tsx
Normal file
142
src/components/TemplateAttributeDialog.tsx
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { ChatStore } from "@/types/chatstore";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogFooter,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { ControlledInput } from "@/components/ui/controlled-input";
|
||||||
|
import { tr } from "@/translate";
|
||||||
|
|
||||||
|
interface TemplateAttributeDialogProps {
|
||||||
|
chatStore: ChatStore;
|
||||||
|
onSave: (name: string, selectedAttributes: Partial<ChatStore>) => void;
|
||||||
|
onClose: () => void;
|
||||||
|
open: boolean;
|
||||||
|
langCode: "en-US" | "zh-CN";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TemplateAttributeDialog({
|
||||||
|
chatStore,
|
||||||
|
onSave,
|
||||||
|
onClose,
|
||||||
|
open,
|
||||||
|
langCode,
|
||||||
|
}: TemplateAttributeDialogProps) {
|
||||||
|
// Create a map of all ChatStore attributes and their selection state
|
||||||
|
const [selectedAttributes, setSelectedAttributes] = useState<Record<string, boolean>>(() => {
|
||||||
|
const initial: Record<string, boolean> = {};
|
||||||
|
// Initialize all attributes as selected by default
|
||||||
|
Object.keys(chatStore).forEach((key) => {
|
||||||
|
initial[key] = true;
|
||||||
|
});
|
||||||
|
return initial;
|
||||||
|
});
|
||||||
|
|
||||||
|
const [templateName, setTemplateName] = useState("");
|
||||||
|
const [nameError, setNameError] = useState("");
|
||||||
|
|
||||||
|
const handleSave = () => {
|
||||||
|
// Validate name
|
||||||
|
if (!templateName.trim()) {
|
||||||
|
setNameError(tr("Template name is required", langCode));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setNameError("");
|
||||||
|
|
||||||
|
// Create a new object with only the selected attributes
|
||||||
|
const filteredStore = {} as Partial<ChatStore>;
|
||||||
|
Object.entries(selectedAttributes).forEach(([key, isSelected]) => {
|
||||||
|
if (isSelected) {
|
||||||
|
const typedKey = key as keyof ChatStore;
|
||||||
|
// Use type assertion to ensure type safety
|
||||||
|
(filteredStore as any)[typedKey] = chatStore[typedKey];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
onSave(templateName, filteredStore);
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleAll = (checked: boolean) => {
|
||||||
|
const newSelected = { ...selectedAttributes };
|
||||||
|
Object.keys(newSelected).forEach((key) => {
|
||||||
|
newSelected[key] = checked;
|
||||||
|
});
|
||||||
|
setSelectedAttributes(newSelected);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={onClose}>
|
||||||
|
<DialogContent className="max-w-2xl">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Select Template Attributes</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Choose which attributes to include in your template. Unselected attributes will be omitted.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="template-name">Template Name</Label>
|
||||||
|
<ControlledInput
|
||||||
|
id="template-name"
|
||||||
|
value={templateName}
|
||||||
|
onChange={(e) => {
|
||||||
|
setTemplateName(e.target.value);
|
||||||
|
setNameError("");
|
||||||
|
}}
|
||||||
|
placeholder={tr("Enter template name", langCode)}
|
||||||
|
/>
|
||||||
|
{nameError && (
|
||||||
|
<p className="text-sm text-red-500">{nameError}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Checkbox
|
||||||
|
id="select-all"
|
||||||
|
checked={Object.values(selectedAttributes).every((v) => v)}
|
||||||
|
onCheckedChange={(checked) => toggleAll(checked as boolean)}
|
||||||
|
/>
|
||||||
|
<Label htmlFor="select-all">Select All</Label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ScrollArea className="h-[400px] rounded-md border p-4">
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
{Object.keys(chatStore).map((key) => (
|
||||||
|
<div key={key} className="flex items-center space-x-2">
|
||||||
|
<Checkbox
|
||||||
|
id={key}
|
||||||
|
checked={selectedAttributes[key]}
|
||||||
|
onCheckedChange={(checked) =>
|
||||||
|
setSelectedAttributes((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[key]: checked as boolean,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Label htmlFor={key} className="text-sm">
|
||||||
|
{key}
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" onClick={onClose}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleSave}>Save Template</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
21
src/components/ui/controlled-input.tsx
Normal file
21
src/components/ui/controlled-input.tsx
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { ComponentProps, forwardRef } from "react";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const ControlledInput = forwardRef<HTMLInputElement, ComponentProps<"input">>(
|
||||||
|
({ className, type, ...props }, ref) => {
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
type={type}
|
||||||
|
className={cn(
|
||||||
|
"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
ControlledInput.displayName = "ControlledInput";
|
||||||
|
|
||||||
|
export { ControlledInput };
|
||||||
Reference in New Issue
Block a user