fix: UI/UX improvements for model configuration dialog

- Add gradient header styling with icon badge
- Change Configuration section icon from Key to Settings2
- Add duplicate model detection with warning banner and inline removal
- Filter out already-added models from suggestions dropdown
- Add type-to-confirm for deleting providers with 3+ models
- Enhance delete confirmation dialog with warning icon
- Improve model selector discoverability (show model name + chevron)
- Add truncation for long model names with title tooltip
- Remove AI provider settings from Settings dialog (now in Model Config)
- Extract ValidationButton into reusable component
This commit is contained in:
dayuan.jiang
2025-12-22 21:49:29 +09:00
parent 7cf6d7e7bd
commit 0e8783ccfb
4 changed files with 177 additions and 43 deletions

View File

@@ -1388,7 +1388,6 @@ Continue from EXACTLY where you stopped.`,
open={showSettingsDialog} open={showSettingsDialog}
onOpenChange={setShowSettingsDialog} onOpenChange={setShowSettingsDialog}
onCloseProtectionChange={onCloseProtectionChange} onCloseProtectionChange={onCloseProtectionChange}
onOpenModelConfig={() => setShowModelConfigDialog(true)}
drawioUi={drawioUi} drawioUi={drawioUi}
onToggleDrawioUi={onToggleDrawioUi} onToggleDrawioUi={onToggleDrawioUi}
darkMode={darkMode} darkMode={darkMode}

View File

@@ -13,6 +13,7 @@ import {
Loader2, Loader2,
Plus, Plus,
Server, Server,
Settings2,
Sparkles, Sparkles,
Tag, Tag,
Trash2, Trash2,
@@ -100,6 +101,42 @@ function ProviderLogo({
) )
} }
// Reusable validation button component
function ValidationButton({
status,
onClick,
disabled,
}: {
status: ValidationStatus
onClick: () => void
disabled: boolean
}) {
return (
<Button
variant={status === "success" ? "outline" : "default"}
size="sm"
onClick={onClick}
disabled={disabled}
className={cn(
"h-9 px-4 min-w-[80px]",
status === "success" &&
"text-emerald-600 border-emerald-200 dark:border-emerald-800",
)}
>
{status === "validating" ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : status === "success" ? (
<>
<Check className="h-4 w-4 mr-1.5" />
Verified
</>
) : (
"Test"
)}
</Button>
)
}
export function ModelConfigDialog({ export function ModelConfigDialog({
open, open,
onOpenChange, onOpenChange,
@@ -120,6 +157,7 @@ export function ModelConfigDialog({
typeof setTimeout typeof setTimeout
> | null>(null) > | null>(null)
const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false) const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false)
const [deleteConfirmText, setDeleteConfirmText] = useState("")
const [validatingModelIndex, setValidatingModelIndex] = useState< const [validatingModelIndex, setValidatingModelIndex] = useState<
number | null number | null
>(null) >(null)
@@ -173,6 +211,26 @@ export function ModelConfigDialog({
? SUGGESTED_MODELS[selectedProvider.provider] || [] ? SUGGESTED_MODELS[selectedProvider.provider] || []
: [] : []
// Filter out already-added models from suggestions
const existingModelIds =
selectedProvider?.models.map((m) => m.modelId) || []
const availableSuggestions = suggestedModels.filter(
(modelId) => !existingModelIds.includes(modelId),
)
// Detect duplicate models in current config
const modelIdCounts =
selectedProvider?.models.reduce(
(acc, m) => {
acc[m.modelId] = (acc[m.modelId] || 0) + 1
return acc
},
{} as Record<string, number>,
) || {}
const duplicateModelIds = Object.keys(modelIdCounts).filter(
(id) => modelIdCounts[id] > 1,
)
// Handle adding a new provider // Handle adding a new provider
const handleAddProvider = (providerType: ProviderName) => { const handleAddProvider = (providerType: ProviderName) => {
const newProvider = addProvider(providerType) const newProvider = addProvider(providerType)
@@ -329,9 +387,11 @@ export function ModelConfigDialog({
return ( return (
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-3xl h-[75vh] max-h-[700px] overflow-hidden flex flex-col gap-0 p-0"> <DialogContent className="sm:max-w-3xl h-[75vh] max-h-[700px] overflow-hidden flex flex-col gap-0 p-0">
<DialogHeader className="px-6 pt-6 pb-4 border-b bg-muted/30"> <DialogHeader className="px-6 pt-6 pb-4 border-b bg-gradient-to-r from-primary/5 via-primary/3 to-transparent">
<DialogTitle className="flex items-center gap-2 text-lg"> <DialogTitle className="flex items-center gap-2.5 text-xl font-semibold">
<Server className="h-5 w-5 text-primary" /> <div className="p-1.5 rounded-lg bg-primary/10">
<Server className="h-5 w-5 text-primary" />
</div>
{dict.modelConfig?.title || "AI Model Configuration"} {dict.modelConfig?.title || "AI Model Configuration"}
</DialogTitle> </DialogTitle>
<DialogDescription className="text-sm"> <DialogDescription className="text-sm">
@@ -508,7 +568,7 @@ export function ModelConfigDialog({
{/* Configuration Section */} {/* Configuration Section */}
<div className="space-y-4"> <div className="space-y-4">
<div className="flex items-center gap-2 text-sm font-medium text-muted-foreground"> <div className="flex items-center gap-2 text-sm font-medium text-muted-foreground">
<Key className="h-4 w-4" /> <Settings2 className="h-4 w-4" />
<span>Configuration</span> <span>Configuration</span>
</div> </div>
@@ -987,14 +1047,21 @@ export function ModelConfigDialog({
) )
} }
}} }}
disabled={
availableSuggestions.length ===
0
}
> >
<SelectTrigger className="w-32 h-8 hover:bg-accent"> <SelectTrigger className="w-32 h-8 hover:bg-accent">
<span className="text-xs"> <span className="text-xs">
Suggested {availableSuggestions.length ===
0
? "All added"
: "Suggested"}
</span> </span>
</SelectTrigger> </SelectTrigger>
<SelectContent className="max-h-72"> <SelectContent className="max-h-72">
{suggestedModels.map( {availableSuggestions.map(
(modelId) => ( (modelId) => (
<SelectItem <SelectItem
key={ key={
@@ -1016,6 +1083,26 @@ export function ModelConfigDialog({
</div> </div>
</div> </div>
{/* Duplicate Warning Banner */}
{duplicateModelIds.length > 0 && (
<div className="px-3 py-2 bg-amber-50 dark:bg-amber-950/30 border border-amber-200 dark:border-amber-800 rounded-lg text-xs text-amber-700 dark:text-amber-400 flex items-center gap-2">
<AlertCircle className="h-4 w-4 flex-shrink-0" />
<span>
{
duplicateModelIds.length
}{" "}
duplicate model
{duplicateModelIds.length >
1
? "s"
: ""}{" "}
detected. Remove
duplicates to avoid
confusion.
</span>
</div>
)}
{/* Model List */} {/* Model List */}
<div className="rounded-xl border bg-card overflow-hidden min-h-[120px]"> <div className="rounded-xl border bg-card overflow-hidden min-h-[120px]">
{selectedProvider.models {selectedProvider.models
@@ -1049,7 +1136,7 @@ export function ModelConfigDialog({
"rounded-b-xl", "rounded-b-xl",
)} )}
> >
<div className="flex items-center gap-3 p-3"> <div className="flex items-center gap-3 p-3 min-w-0">
{/* Status icon */} {/* Status icon */}
<div className="flex items-center justify-center w-8 h-8 rounded-lg flex-shrink-0"> <div className="flex items-center justify-center w-8 h-8 rounded-lg flex-shrink-0">
{validatingModelIndex !== {validatingModelIndex !==
@@ -1093,6 +1180,9 @@ export function ModelConfigDialog({
value={ value={
model.modelId model.modelId
} }
title={
model.modelId
}
onChange={( onChange={(
e, e,
) => { ) => {
@@ -1111,7 +1201,7 @@ export function ModelConfigDialog({
}, },
) )
}} }}
className="flex-1 font-mono text-sm h-8 border-0 bg-transparent focus-visible:bg-background focus-visible:ring-1" className="flex-1 min-w-0 font-mono text-sm h-8 border-0 bg-transparent focus-visible:bg-background focus-visible:ring-1"
/> />
<Button <Button
variant="ghost" variant="ghost"
@@ -1137,6 +1227,29 @@ export function ModelConfigDialog({
} }
</p> </p>
)} )}
{/* Show duplicate warning inline */}
{duplicateModelIds.includes(
model.modelId,
) && (
<div className="flex items-center gap-2 px-3 pb-2 pl-14">
<span className="text-xs text-amber-600 dark:text-amber-400 flex items-center gap-1">
<AlertCircle className="h-3 w-3" />
Duplicate
</span>
<Button
variant="ghost"
size="sm"
className="h-5 px-2 text-xs text-destructive hover:text-destructive hover:bg-destructive/10"
onClick={() =>
handleDeleteModel(
model.id,
)
}
>
Remove
</Button>
</div>
)}
</div> </div>
), ),
)} )}
@@ -1191,12 +1304,20 @@ export function ModelConfigDialog({
{/* Delete Confirmation Dialog */} {/* Delete Confirmation Dialog */}
<AlertDialog <AlertDialog
open={deleteConfirmOpen} open={deleteConfirmOpen}
onOpenChange={setDeleteConfirmOpen} onOpenChange={(open) => {
setDeleteConfirmOpen(open)
if (!open) setDeleteConfirmText("")
}}
> >
<AlertDialogContent> <AlertDialogContent className="border-destructive/30">
<AlertDialogHeader> <AlertDialogHeader>
<AlertDialogTitle>Delete Provider</AlertDialogTitle> <div className="mx-auto mb-3 p-3 rounded-full bg-destructive/10">
<AlertDialogDescription> <AlertCircle className="h-6 w-6 text-destructive" />
</div>
<AlertDialogTitle className="text-center">
Delete Provider
</AlertDialogTitle>
<AlertDialogDescription className="text-center">
Are you sure you want to delete{" "} Are you sure you want to delete{" "}
<span className="font-medium text-foreground"> <span className="font-medium text-foreground">
{selectedProvider {selectedProvider
@@ -1209,11 +1330,43 @@ export function ModelConfigDialog({
be undone. be undone.
</AlertDialogDescription> </AlertDialogDescription>
</AlertDialogHeader> </AlertDialogHeader>
{selectedProvider &&
selectedProvider.models.length >= 3 && (
<div className="mt-2 space-y-2">
<Label
htmlFor="delete-confirm"
className="text-sm text-muted-foreground"
>
Type &quot;
{selectedProvider.name ||
PROVIDER_INFO[selectedProvider.provider]
.label}
&quot; to confirm
</Label>
<Input
id="delete-confirm"
value={deleteConfirmText}
onChange={(e) =>
setDeleteConfirmText(e.target.value)
}
placeholder="Type provider name..."
className="h-9"
/>
</div>
)}
<AlertDialogFooter> <AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel> <AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction <AlertDialogAction
onClick={handleDeleteProvider} onClick={handleDeleteProvider}
className="bg-destructive text-destructive-foreground hover:bg-destructive/90" disabled={
selectedProvider &&
selectedProvider.models.length >= 3 &&
deleteConfirmText !==
(selectedProvider.name ||
PROVIDER_INFO[selectedProvider.provider]
.label)
}
className="bg-destructive text-destructive-foreground hover:bg-destructive/90 disabled:opacity-50"
> >
Delete Delete
</AlertDialogAction> </AlertDialogAction>

View File

@@ -1,6 +1,6 @@
"use client" "use client"
import { Bot, Check, Server, Settings2 } from "lucide-react" import { Bot, Check, ChevronDown, Server, Settings2 } from "lucide-react"
import { useMemo, useState } from "react" import { useMemo, useState } from "react"
import { import {
ModelSelectorContent, ModelSelectorContent,
@@ -96,8 +96,8 @@ export function ModelSelector({
} }
const tooltipContent = selectedModel const tooltipContent = selectedModel
? `Model: ${selectedModel.modelId}` ? `${selectedModel.modelId} (click to change)`
: "Model: Server Default" : "Using server default model (click to change)"
return ( return (
<ModelSelectorRoot open={open} onOpenChange={setOpen}> <ModelSelectorRoot open={open} onOpenChange={setOpen}>
@@ -105,11 +105,15 @@ export function ModelSelector({
<ButtonWithTooltip <ButtonWithTooltip
tooltipContent={tooltipContent} tooltipContent={tooltipContent}
variant="ghost" variant="ghost"
size="icon" size="sm"
disabled={disabled} disabled={disabled}
className="hover:bg-accent" className="hover:bg-accent gap-1.5 h-8 max-w-[180px] px-2"
> >
<Bot className="h-5 w-5 text-muted-foreground" /> <Bot className="h-4 w-4 flex-shrink-0 text-muted-foreground" />
<span className="text-xs truncate">
{selectedModel ? selectedModel.modelId : "Default"}
</span>
<ChevronDown className="h-3 w-3 flex-shrink-0 text-muted-foreground" />
</ButtonWithTooltip> </ButtonWithTooltip>
</ModelSelectorTrigger> </ModelSelectorTrigger>
<ModelSelectorContent title="Select Model"> <ModelSelectorContent title="Select Model">

View File

@@ -1,6 +1,6 @@
"use client" "use client"
import { Moon, Settings2, Sun } from "lucide-react" import { Moon, Sun } from "lucide-react"
import { useEffect, useState } from "react" import { useEffect, useState } from "react"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { import {
@@ -19,7 +19,6 @@ interface SettingsDialogProps {
open: boolean open: boolean
onOpenChange: (open: boolean) => void onOpenChange: (open: boolean) => void
onCloseProtectionChange?: (enabled: boolean) => void onCloseProtectionChange?: (enabled: boolean) => void
onOpenModelConfig: () => void
drawioUi: "min" | "sketch" drawioUi: "min" | "sketch"
onToggleDrawioUi: () => void onToggleDrawioUi: () => void
darkMode: boolean darkMode: boolean
@@ -41,7 +40,6 @@ export function SettingsDialog({
open, open,
onOpenChange, onOpenChange,
onCloseProtectionChange, onCloseProtectionChange,
onOpenModelConfig,
drawioUi, drawioUi,
onToggleDrawioUi, onToggleDrawioUi,
darkMode, darkMode,
@@ -178,26 +176,6 @@ export function SettingsDialog({
)} )}
</div> </div>
)} )}
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label>{dict.settings.aiProvider}</Label>
<p className="text-[0.8rem] text-muted-foreground">
{dict.settings.aiProviderDescription}
</p>
</div>
<Button
variant="outline"
size="sm"
onClick={() => {
onOpenChange(false)
onOpenModelConfig()
}}
>
<Settings2 className="h-4 w-4 mr-2" />
{dict.modelConfig?.configure || "Configure"}
</Button>
</div>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="space-y-0.5"> <div className="space-y-0.5">
<Label htmlFor="theme-toggle"> <Label htmlFor="theme-toggle">