Disable auto-update setting & settings page has scroll shortcuts (#590)
Fixes https://github.com/dyad-sh/dyad/issues/561
This commit is contained in:
36
src/components/AutoUpdateSwitch.tsx
Normal file
36
src/components/AutoUpdateSwitch.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import { useSettings } from "@/hooks/useSettings";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { toast } from "sonner";
|
||||
import { IpcClient } from "@/ipc/ipc_client";
|
||||
|
||||
export function AutoUpdateSwitch() {
|
||||
const { settings, updateSettings } = useSettings();
|
||||
|
||||
if (!settings) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center space-x-2">
|
||||
<Switch
|
||||
id="enable-auto-update"
|
||||
checked={settings.enableAutoUpdate}
|
||||
onCheckedChange={(checked) => {
|
||||
updateSettings({ enableAutoUpdate: checked });
|
||||
toast("Auto-update settings changed", {
|
||||
description:
|
||||
"You will need to restart Dyad for your settings to take effect.",
|
||||
action: {
|
||||
label: "Restart Dyad",
|
||||
onClick: () => {
|
||||
IpcClient.getInstance().restartDyad();
|
||||
},
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<Label htmlFor="enable-auto-update">Auto-update</Label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -68,7 +68,7 @@ export function ProviderSettingsGrid() {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="p-6">
|
||||
<h2 className="text-2xl font-bold mb-6">AI Providers</h2>
|
||||
<h2 className="text-lg font-medium mb-6">AI Providers</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{[1, 2, 3, 4, 5].map((i) => (
|
||||
<Card key={i} className="border-border">
|
||||
@@ -86,7 +86,7 @@ export function ProviderSettingsGrid() {
|
||||
if (error) {
|
||||
return (
|
||||
<div className="p-6">
|
||||
<h2 className="text-2xl font-bold mb-6">AI Providers</h2>
|
||||
<h2 className="text-lg font-medium mb-6">AI Providers</h2>
|
||||
<Alert variant="destructive">
|
||||
<AlertTriangle className="h-4 w-4" />
|
||||
<AlertTitle>Error</AlertTitle>
|
||||
@@ -100,7 +100,7 @@ export function ProviderSettingsGrid() {
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
<h2 className="text-2xl font-bold mb-6">AI Providers</h2>
|
||||
<h2 className="text-lg font-medium mb-6">AI Providers</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{providers
|
||||
?.filter((p) => p.type !== "local")
|
||||
@@ -116,7 +116,7 @@ export function ProviderSettingsGrid() {
|
||||
className="p-4 cursor-pointer"
|
||||
onClick={() => handleProviderClick(provider.id)}
|
||||
>
|
||||
<CardTitle className="text-xl flex items-center justify-between">
|
||||
<CardTitle className="text-lg font-medium flex items-center justify-between">
|
||||
{provider.name}
|
||||
{isProviderSetup(provider.id) ? (
|
||||
<span className="ml-3 text-sm font-medium text-green-500 bg-green-50 dark:bg-green-900/30 border border-green-500/50 dark:border-green-500/50 px-2 py-1 rounded-full">
|
||||
@@ -178,8 +178,8 @@ export function ProviderSettingsGrid() {
|
||||
onClick={() => setIsDialogOpen(true)}
|
||||
>
|
||||
<CardHeader className="p-4 flex flex-col items-center justify-center h-full">
|
||||
<PlusIcon className="h-10 w-10 text-muted-foreground mb-2" />
|
||||
<CardTitle className="text-xl text-center">
|
||||
<PlusIcon className="h-8 w-8 text-muted-foreground mb-2" />
|
||||
<CardTitle className="text-lg font-medium text-center">
|
||||
Add custom provider
|
||||
</CardTitle>
|
||||
<CardDescription className="text-center">
|
||||
|
||||
88
src/components/SettingsList.tsx
Normal file
88
src/components/SettingsList.tsx
Normal file
@@ -0,0 +1,88 @@
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
const SETTINGS_SECTIONS = [
|
||||
{ id: "general-settings", label: "General" },
|
||||
{ id: "workflow-settings", label: "Workflow" },
|
||||
{ id: "ai-settings", label: "AI" },
|
||||
{ id: "provider-settings", label: "Model Providers" },
|
||||
{ id: "telemetry", label: "Telemetry" },
|
||||
{ id: "integrations", label: "Integrations" },
|
||||
{ id: "experiments", label: "Experiments" },
|
||||
{ id: "danger-zone", label: "Danger Zone" },
|
||||
];
|
||||
|
||||
export function SettingsList({ show }: { show: boolean }) {
|
||||
const navigate = useNavigate();
|
||||
const [activeSection, setActiveSection] = useState<string | null>(
|
||||
"general-settings",
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
for (const entry of entries) {
|
||||
if (entry.isIntersecting) {
|
||||
setActiveSection(entry.target.id);
|
||||
return;
|
||||
}
|
||||
}
|
||||
},
|
||||
{ rootMargin: "-20% 0px -80% 0px", threshold: 0 },
|
||||
);
|
||||
|
||||
for (const section of SETTINGS_SECTIONS) {
|
||||
const el = document.getElementById(section.id);
|
||||
if (el) {
|
||||
observer.observe(el);
|
||||
}
|
||||
}
|
||||
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (!show) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handleScrollAndNavigateTo = async (id: string) => {
|
||||
await navigate({
|
||||
to: "/settings",
|
||||
});
|
||||
const element = document.getElementById(id);
|
||||
if (element) {
|
||||
element.scrollIntoView({ behavior: "smooth", block: "start" });
|
||||
setActiveSection(id);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
<div className="flex-shrink-0 p-4">
|
||||
<h2 className="text-lg font-semibold tracking-tight">Settings</h2>
|
||||
</div>
|
||||
<ScrollArea className="flex-grow">
|
||||
<div className="space-y-1 p-4 pt-0">
|
||||
{SETTINGS_SECTIONS.map((section) => (
|
||||
<button
|
||||
key={section.id}
|
||||
onClick={() => handleScrollAndNavigateTo(section.id)}
|
||||
className={cn(
|
||||
"w-full text-left px-3 py-2 rounded-md text-sm transition-colors",
|
||||
activeSection === section.id
|
||||
? "bg-sidebar-accent text-sidebar-accent-foreground font-semibold"
|
||||
: "hover:bg-sidebar-accent",
|
||||
)}
|
||||
>
|
||||
{section.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
import { ChatList } from "./ChatList";
|
||||
import { AppList } from "./AppList";
|
||||
import { HelpDialog } from "./HelpDialog"; // Import the new dialog
|
||||
import { SettingsList } from "./SettingsList";
|
||||
|
||||
// Menu items.
|
||||
const items = [
|
||||
@@ -49,6 +50,7 @@ const items = [
|
||||
type HoverState =
|
||||
| "start-hover:app"
|
||||
| "start-hover:chat"
|
||||
| "start-hover:settings"
|
||||
| "clear-hover"
|
||||
| "no-hover";
|
||||
|
||||
@@ -60,10 +62,7 @@ export function AppSidebar() {
|
||||
const [isDropdownOpen] = useAtom(dropdownOpenAtom);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
(hoverState === "start-hover:app" || hoverState === "start-hover:chat") &&
|
||||
state === "collapsed"
|
||||
) {
|
||||
if (hoverState.startsWith("start-hover") && state === "collapsed") {
|
||||
expandedByHover.current = true;
|
||||
toggleSidebar();
|
||||
}
|
||||
@@ -84,17 +83,22 @@ export function AppSidebar() {
|
||||
routerState.location.pathname === "/" ||
|
||||
routerState.location.pathname.startsWith("/app-details");
|
||||
const isChatRoute = routerState.location.pathname === "/chat";
|
||||
const isSettingsRoute = routerState.location.pathname.startsWith("/settings");
|
||||
|
||||
let selectedItem: string | null = null;
|
||||
if (hoverState === "start-hover:app") {
|
||||
selectedItem = "Apps";
|
||||
} else if (hoverState === "start-hover:chat") {
|
||||
selectedItem = "Chat";
|
||||
} else if (hoverState === "start-hover:settings") {
|
||||
selectedItem = "Settings";
|
||||
} else if (state === "expanded") {
|
||||
if (isAppRoute) {
|
||||
selectedItem = "Apps";
|
||||
} else if (isChatRoute) {
|
||||
selectedItem = "Chat";
|
||||
} else if (isSettingsRoute) {
|
||||
selectedItem = "Settings";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,6 +126,7 @@ export function AppSidebar() {
|
||||
<div className="w-[240px]">
|
||||
<AppList show={selectedItem === "Apps"} />
|
||||
<ChatList show={selectedItem === "Chat"} />
|
||||
<SettingsList show={selectedItem === "Settings"} />
|
||||
</div>
|
||||
</div>
|
||||
</SidebarContent>
|
||||
@@ -188,6 +193,8 @@ function AppIcons({
|
||||
onHoverChange("start-hover:app");
|
||||
} else if (item.title === "Chat") {
|
||||
onHoverChange("start-hover:chat");
|
||||
} else if (item.title === "Settings") {
|
||||
onHoverChange("start-hover:settings");
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
||||
46
src/components/ui/scroll-area.tsx
Normal file
46
src/components/ui/scroll-area.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import * as React from "react";
|
||||
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const ScrollArea = React.forwardRef<
|
||||
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<ScrollAreaPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn("relative overflow-hidden", className)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
|
||||
{children}
|
||||
</ScrollAreaPrimitive.Viewport>
|
||||
<ScrollBar />
|
||||
<ScrollAreaPrimitive.Corner />
|
||||
</ScrollAreaPrimitive.Root>
|
||||
));
|
||||
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName;
|
||||
|
||||
const ScrollBar = React.forwardRef<
|
||||
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
|
||||
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||
>(({ className, orientation = "vertical", ...props }, ref) => (
|
||||
<ScrollAreaPrimitive.ScrollAreaScrollbar
|
||||
ref={ref}
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"flex touch-none select-none transition-colors",
|
||||
orientation === "vertical" &&
|
||||
"h-full w-2.5 border-l border-l-transparent p-[1px]",
|
||||
orientation === "horizontal" &&
|
||||
"h-2.5 border-t border-t-transparent p-[1px]",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
|
||||
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||
));
|
||||
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName;
|
||||
|
||||
export { ScrollArea, ScrollBar };
|
||||
Reference in New Issue
Block a user