mirror of
https://github.com/DayuanJiang/next-ai-draw-io.git
synced 2026-01-03 23:02:31 +08:00
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:
@@ -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}
|
||||||
|
|||||||
@@ -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">
|
||||||
|
<div className="p-1.5 rounded-lg bg-primary/10">
|
||||||
<Server className="h-5 w-5 text-primary" />
|
<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 "
|
||||||
|
{selectedProvider.name ||
|
||||||
|
PROVIDER_INFO[selectedProvider.provider]
|
||||||
|
.label}
|
||||||
|
" 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>
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
Reference in New Issue
Block a user