feat: add configurable close protection setting (#123)

- Add Close Protection toggle to Settings dialog
- Save setting to localStorage (default: enabled)
- Make beforeunload confirmation conditional
- Settings button now always visible in header
- Add shadcn Switch and Label components
This commit is contained in:
Dayuan Jiang
2025-12-06 21:42:28 +09:00
committed by GitHub
parent 77f2569a3b
commit 9f77199272
7 changed files with 358 additions and 15 deletions

View File

@@ -31,6 +31,7 @@ interface ChatPanelProps {
drawioUi: "min" | "sketch"
onToggleDrawioUi: () => void
isMobile?: boolean
onCloseProtectionChange?: (enabled: boolean) => void
}
export default function ChatPanel({
@@ -39,6 +40,7 @@ export default function ChatPanel({
drawioUi,
onToggleDrawioUi,
isMobile = false,
onCloseProtectionChange,
}: ChatPanelProps) {
const {
loadDiagram: onDisplayChart,
@@ -497,19 +499,17 @@ Please retry with an adjusted search pattern or use display_diagram if retries a
className={`${isMobile ? "w-4 h-4" : "w-5 h-5"}`}
/>
</a>
{accessCodeRequired && (
<ButtonWithTooltip
tooltipContent="Settings"
variant="ghost"
size="icon"
onClick={() => setShowSettingsDialog(true)}
className="hover:bg-accent"
>
<Settings
className={`${isMobile ? "h-4 w-4" : "h-5 w-5"} text-muted-foreground`}
/>
</ButtonWithTooltip>
)}
<ButtonWithTooltip
tooltipContent="Settings"
variant="ghost"
size="icon"
onClick={() => setShowSettingsDialog(true)}
className="hover:bg-accent"
>
<Settings
className={`${isMobile ? "h-4 w-4" : "h-5 w-5"} text-muted-foreground`}
/>
</ButtonWithTooltip>
{!isMobile && (
<ButtonWithTooltip
tooltipContent="Hide chat panel (Ctrl+B)"
@@ -570,6 +570,7 @@ Please retry with an adjusted search pattern or use display_diagram if retries a
<SettingsDialog
open={showSettingsDialog}
onOpenChange={setShowSettingsDialog}
onCloseProtectionChange={onCloseProtectionChange}
/>
</div>
)

View File

@@ -11,27 +11,47 @@ import {
DialogTitle,
} from "@/components/ui/dialog"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Switch } from "@/components/ui/switch"
interface SettingsDialogProps {
open: boolean
onOpenChange: (open: boolean) => void
onCloseProtectionChange?: (enabled: boolean) => void
}
export const STORAGE_ACCESS_CODE_KEY = "next-ai-draw-io-access-code"
export const STORAGE_CLOSE_PROTECTION_KEY = "next-ai-draw-io-close-protection"
export function SettingsDialog({ open, onOpenChange }: SettingsDialogProps) {
export function SettingsDialog({
open,
onOpenChange,
onCloseProtectionChange,
}: SettingsDialogProps) {
const [accessCode, setAccessCode] = useState("")
const [closeProtection, setCloseProtection] = useState(true)
useEffect(() => {
if (open) {
const storedCode =
localStorage.getItem(STORAGE_ACCESS_CODE_KEY) || ""
setAccessCode(storedCode)
const storedCloseProtection = localStorage.getItem(
STORAGE_CLOSE_PROTECTION_KEY,
)
// Default to true if not set
setCloseProtection(storedCloseProtection !== "false")
}
}, [open])
const handleSave = () => {
localStorage.setItem(STORAGE_ACCESS_CODE_KEY, accessCode.trim())
localStorage.setItem(
STORAGE_CLOSE_PROTECTION_KEY,
closeProtection.toString(),
)
onCloseProtectionChange?.(closeProtection)
onOpenChange(false)
}
@@ -68,6 +88,21 @@ export function SettingsDialog({ open, onOpenChange }: SettingsDialogProps) {
Required if the server has enabled access control.
</p>
</div>
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label htmlFor="close-protection">
Close Protection
</Label>
<p className="text-[0.8rem] text-muted-foreground">
Show confirmation when leaving the page.
</p>
</div>
<Switch
id="close-protection"
checked={closeProtection}
onCheckedChange={setCloseProtection}
/>
</div>
</div>
<DialogFooter>
<Button

24
components/ui/label.tsx Normal file
View File

@@ -0,0 +1,24 @@
"use client"
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { cn } from "@/lib/utils"
function Label({
className,
...props
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
return (
<LabelPrimitive.Root
data-slot="label"
className={cn(
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
className
)}
{...props}
/>
)
}
export { Label }

31
components/ui/switch.tsx Normal file
View File

@@ -0,0 +1,31 @@
"use client"
import * as React from "react"
import * as SwitchPrimitive from "@radix-ui/react-switch"
import { cn } from "@/lib/utils"
function Switch({
className,
...props
}: React.ComponentProps<typeof SwitchPrimitive.Root>) {
return (
<SwitchPrimitive.Root
data-slot="switch"
className={cn(
"peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
>
<SwitchPrimitive.Thumb
data-slot="switch-thumb"
className={cn(
"bg-background dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block size-4 rounded-full ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0"
)}
/>
</SwitchPrimitive.Root>
)
}
export { Switch }