mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-05-05 05:12:38 +02:00
add settings dialog box
This commit is contained in:
parent
a89561db9a
commit
5487077f1f
2 changed files with 237 additions and 12 deletions
229
apps/x/apps/renderer/src/components/settings-dialog.tsx
Normal file
229
apps/x/apps/renderer/src/components/settings-dialog.tsx
Normal file
|
|
@ -0,0 +1,229 @@
|
|||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { useState, useEffect } from "react"
|
||||
import { Server, Key, Shield } from "lucide-react"
|
||||
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
type ConfigTab = "models" | "mcp" | "security"
|
||||
|
||||
interface TabConfig {
|
||||
id: ConfigTab
|
||||
label: string
|
||||
icon: React.ElementType
|
||||
path: string
|
||||
description: string
|
||||
}
|
||||
|
||||
const tabs: TabConfig[] = [
|
||||
{
|
||||
id: "models",
|
||||
label: "Models",
|
||||
icon: Key,
|
||||
path: "config/models.json",
|
||||
description: "Configure LLM providers and API keys",
|
||||
},
|
||||
{
|
||||
id: "mcp",
|
||||
label: "MCP Servers",
|
||||
icon: Server,
|
||||
path: "config/mcp.json",
|
||||
description: "Configure MCP server connections",
|
||||
},
|
||||
{
|
||||
id: "security",
|
||||
label: "Security",
|
||||
icon: Shield,
|
||||
path: "config/security.json",
|
||||
description: "Configure allowed shell commands",
|
||||
},
|
||||
]
|
||||
|
||||
interface SettingsDialogProps {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export function SettingsDialog({ children }: SettingsDialogProps) {
|
||||
const [open, setOpen] = useState(false)
|
||||
const [activeTab, setActiveTab] = useState<ConfigTab>("models")
|
||||
const [content, setContent] = useState("")
|
||||
const [originalContent, setOriginalContent] = useState("")
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [saving, setSaving] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
const activeTabConfig = tabs.find((t) => t.id === activeTab)!
|
||||
|
||||
const loadConfig = async (tab: ConfigTab) => {
|
||||
const tabConfig = tabs.find((t) => t.id === tab)!
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
try {
|
||||
const result = await window.ipc.invoke("workspace:readFile", {
|
||||
path: tabConfig.path,
|
||||
})
|
||||
const formattedContent = formatJson(result.data)
|
||||
setContent(formattedContent)
|
||||
setOriginalContent(formattedContent)
|
||||
} catch (err) {
|
||||
setError(`Failed to load ${tabConfig.label} config`)
|
||||
setContent("")
|
||||
setOriginalContent("")
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const saveConfig = async () => {
|
||||
setSaving(true)
|
||||
setError(null)
|
||||
try {
|
||||
// Validate JSON before saving
|
||||
JSON.parse(content)
|
||||
await window.ipc.invoke("workspace:writeFile", {
|
||||
path: activeTabConfig.path,
|
||||
data: content,
|
||||
})
|
||||
setOriginalContent(content)
|
||||
} catch (err) {
|
||||
if (err instanceof SyntaxError) {
|
||||
setError("Invalid JSON syntax")
|
||||
} else {
|
||||
setError(`Failed to save ${activeTabConfig.label} config`)
|
||||
}
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
const formatJson = (jsonString: string): string => {
|
||||
try {
|
||||
return JSON.stringify(JSON.parse(jsonString), null, 2)
|
||||
} catch {
|
||||
return jsonString
|
||||
}
|
||||
}
|
||||
|
||||
const handleFormat = () => {
|
||||
setContent(formatJson(content))
|
||||
}
|
||||
|
||||
const hasChanges = content !== originalContent
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
loadConfig(activeTab)
|
||||
}
|
||||
}, [open, activeTab])
|
||||
|
||||
const handleTabChange = (tab: ConfigTab) => {
|
||||
if (hasChanges) {
|
||||
if (!confirm("You have unsaved changes. Discard them?")) {
|
||||
return
|
||||
}
|
||||
}
|
||||
setActiveTab(tab)
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>{children}</DialogTrigger>
|
||||
<DialogContent
|
||||
className="!max-w-[900px] w-[900px] h-[600px] p-0 gap-0 overflow-hidden"
|
||||
showCloseButton={false}
|
||||
>
|
||||
<div className="flex h-full">
|
||||
{/* Sidebar */}
|
||||
<div className="w-48 border-r bg-muted/30 p-2 flex flex-col">
|
||||
<div className="px-2 py-3 mb-2">
|
||||
<h2 className="font-semibold text-sm">Settings</h2>
|
||||
</div>
|
||||
<nav className="flex flex-col gap-1">
|
||||
{tabs.map((tab) => (
|
||||
<button
|
||||
key={tab.id}
|
||||
onClick={() => handleTabChange(tab.id)}
|
||||
className={cn(
|
||||
"flex items-center gap-2 px-2 py-2 rounded-md text-sm transition-colors text-left",
|
||||
activeTab === tab.id
|
||||
? "bg-background text-foreground shadow-sm"
|
||||
: "text-muted-foreground hover:text-foreground hover:bg-background/50"
|
||||
)}
|
||||
>
|
||||
<tab.icon className="size-4" />
|
||||
{tab.label}
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
{/* Main content */}
|
||||
<div className="flex-1 flex flex-col min-w-0">
|
||||
{/* Header */}
|
||||
<div className="px-4 py-3 border-b">
|
||||
<h3 className="font-medium text-sm">{activeTabConfig.label}</h3>
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
{activeTabConfig.description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Editor */}
|
||||
<div className="flex-1 p-4 overflow-hidden">
|
||||
{loading ? (
|
||||
<div className="h-full flex items-center justify-center text-muted-foreground text-sm">
|
||||
Loading...
|
||||
</div>
|
||||
) : (
|
||||
<textarea
|
||||
value={content}
|
||||
onChange={(e) => setContent(e.target.value)}
|
||||
className="w-full h-full resize-none bg-muted/50 rounded-md p-3 font-mono text-sm border-0 focus:outline-none focus:ring-1 focus:ring-ring"
|
||||
spellCheck={false}
|
||||
placeholder="Loading configuration..."
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="px-4 py-3 border-t flex items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
{error && (
|
||||
<span className="text-xs text-destructive">{error}</span>
|
||||
)}
|
||||
{hasChanges && !error && (
|
||||
<span className="text-xs text-muted-foreground">
|
||||
Unsaved changes
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleFormat}
|
||||
disabled={loading || saving}
|
||||
>
|
||||
Format
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={saveConfig}
|
||||
disabled={loading || saving || !hasChanges}
|
||||
>
|
||||
{saving ? "Saving..." : "Save"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
|
@ -18,6 +18,7 @@ import {
|
|||
import { type ActiveSection, useSidebarSection } from "@/contexts/sidebar-context"
|
||||
import { ConnectorsPopover } from "@/components/connectors-popover"
|
||||
import { HelpPopover } from "@/components/help-popover"
|
||||
import { SettingsDialog } from "@/components/settings-dialog"
|
||||
|
||||
type NavItem = {
|
||||
id: ActiveSection
|
||||
|
|
@ -71,18 +72,13 @@ export function SidebarIcon() {
|
|||
</ConnectorsPopover>
|
||||
|
||||
{/* Settings */}
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
className="flex h-10 w-10 items-center justify-center rounded-md text-sidebar-foreground/70 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground transition-colors"
|
||||
>
|
||||
<Settings className="size-5" />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right" sideOffset={8}>
|
||||
Settings
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<SettingsDialog>
|
||||
<button
|
||||
className="flex h-10 w-10 items-center justify-center rounded-md text-sidebar-foreground/70 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground transition-colors"
|
||||
>
|
||||
<Settings className="size-5" />
|
||||
</button>
|
||||
</SettingsDialog>
|
||||
|
||||
{/* Help */}
|
||||
<HelpPopover tooltip="Help">
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue