mirror of
https://github.com/DayuanJiang/next-ai-draw-io.git
synced 2026-01-02 22:32:27 +08:00
fix: improve duplicate model validation UX in config dialog
- Add inline error display for duplicate model IDs - Show red border on input when error exists - Validate on blur with shake animation for edit errors - Prevent saving empty model names - Clear errors when user starts typing - Simplify error styling (small red text, no heavy chips)
This commit is contained in:
@@ -162,6 +162,11 @@ export function ModelConfigDialog({
|
||||
const [validatingModelIndex, setValidatingModelIndex] = useState<
|
||||
number | null
|
||||
>(null)
|
||||
const [duplicateError, setDuplicateError] = useState<string>("")
|
||||
const [editError, setEditError] = useState<{
|
||||
modelId: string
|
||||
message: string
|
||||
} | null>(null)
|
||||
|
||||
const {
|
||||
config,
|
||||
@@ -219,19 +224,6 @@ export function ModelConfigDialog({
|
||||
(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
|
||||
const handleAddProvider = (providerType: ProviderName) => {
|
||||
const newProvider = addProvider(providerType)
|
||||
@@ -261,14 +253,17 @@ export function ModelConfigDialog({
|
||||
}
|
||||
|
||||
// Handle adding a model to current provider
|
||||
const handleAddModel = (modelId: string) => {
|
||||
if (!selectedProviderId || !selectedProvider) return
|
||||
// Returns true if model was added successfully, false otherwise
|
||||
const handleAddModel = (modelId: string): boolean => {
|
||||
if (!selectedProviderId || !selectedProvider) return false
|
||||
// Prevent duplicate model IDs
|
||||
if (existingModelIds.includes(modelId)) {
|
||||
toast.warning(`Model "${modelId}" already exists in this provider`)
|
||||
return
|
||||
setDuplicateError(`Model "${modelId}" already exists`)
|
||||
return false
|
||||
}
|
||||
setDuplicateError("")
|
||||
addModel(selectedProviderId, modelId)
|
||||
return true
|
||||
}
|
||||
|
||||
// Handle deleting a model
|
||||
@@ -997,30 +992,58 @@ export function ModelConfigDialog({
|
||||
<span>Models</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="relative">
|
||||
<Input
|
||||
placeholder="Custom model ID..."
|
||||
value={customModelInput}
|
||||
onChange={(e) =>
|
||||
value={
|
||||
customModelInput
|
||||
}
|
||||
onChange={(e) => {
|
||||
setCustomModelInput(
|
||||
e.target.value,
|
||||
e.target
|
||||
.value,
|
||||
)
|
||||
// Clear duplicate error when typing
|
||||
if (
|
||||
duplicateError
|
||||
) {
|
||||
setDuplicateError(
|
||||
"",
|
||||
)
|
||||
}
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (
|
||||
e.key ===
|
||||
"Enter" &&
|
||||
customModelInput.trim()
|
||||
) {
|
||||
const success =
|
||||
handleAddModel(
|
||||
customModelInput.trim(),
|
||||
)
|
||||
if (
|
||||
success
|
||||
) {
|
||||
setCustomModelInput(
|
||||
"",
|
||||
)
|
||||
}
|
||||
}
|
||||
}}
|
||||
className="h-8 w-48 font-mono text-xs"
|
||||
className={cn(
|
||||
"h-8 w-48 font-mono text-xs",
|
||||
duplicateError &&
|
||||
"border-destructive focus-visible:ring-destructive",
|
||||
)}
|
||||
/>
|
||||
{/* Show duplicate error for custom model input */}
|
||||
{duplicateError && (
|
||||
<p className="absolute top-full left-0 mt-1 text-[11px] text-destructive">
|
||||
{duplicateError}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
@@ -1029,13 +1052,16 @@ export function ModelConfigDialog({
|
||||
if (
|
||||
customModelInput.trim()
|
||||
) {
|
||||
const success =
|
||||
handleAddModel(
|
||||
customModelInput.trim(),
|
||||
)
|
||||
if (success) {
|
||||
setCustomModelInput(
|
||||
"",
|
||||
)
|
||||
}
|
||||
}
|
||||
}}
|
||||
disabled={
|
||||
!customModelInput.trim()
|
||||
@@ -1089,26 +1115,6 @@ export function ModelConfigDialog({
|
||||
</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 */}
|
||||
<div className="rounded-xl border bg-card overflow-hidden min-h-[120px]">
|
||||
{selectedProvider.models
|
||||
@@ -1193,6 +1199,15 @@ export function ModelConfigDialog({
|
||||
e,
|
||||
) => {
|
||||
// Allow free typing - validation happens on blur
|
||||
// Clear edit error when typing
|
||||
if (
|
||||
editError?.modelId ===
|
||||
model.id
|
||||
) {
|
||||
setEditError(
|
||||
null,
|
||||
)
|
||||
}
|
||||
updateModel(
|
||||
selectedProviderId!,
|
||||
model.id,
|
||||
@@ -1208,12 +1223,80 @@ export function ModelConfigDialog({
|
||||
},
|
||||
)
|
||||
}}
|
||||
onKeyDown={(
|
||||
e,
|
||||
) => {
|
||||
if (
|
||||
e.key ===
|
||||
"Enter"
|
||||
) {
|
||||
e.currentTarget.blur()
|
||||
}
|
||||
}}
|
||||
onBlur={(
|
||||
e,
|
||||
) => {
|
||||
const newModelId =
|
||||
e.target.value.trim()
|
||||
// Check if new ID would be duplicate (excluding current model)
|
||||
|
||||
// Helper to show error with shake
|
||||
const showError =
|
||||
(
|
||||
message: string,
|
||||
) => {
|
||||
setEditError(
|
||||
{
|
||||
modelId:
|
||||
model.id,
|
||||
message,
|
||||
},
|
||||
)
|
||||
e.target.animate(
|
||||
[
|
||||
{
|
||||
transform:
|
||||
"translateX(0)",
|
||||
},
|
||||
{
|
||||
transform:
|
||||
"translateX(-4px)",
|
||||
},
|
||||
{
|
||||
transform:
|
||||
"translateX(4px)",
|
||||
},
|
||||
{
|
||||
transform:
|
||||
"translateX(-4px)",
|
||||
},
|
||||
{
|
||||
transform:
|
||||
"translateX(4px)",
|
||||
},
|
||||
{
|
||||
transform:
|
||||
"translateX(0)",
|
||||
},
|
||||
],
|
||||
{
|
||||
duration: 400,
|
||||
easing: "ease-in-out",
|
||||
},
|
||||
)
|
||||
e.target.focus()
|
||||
}
|
||||
|
||||
// Check for empty model name
|
||||
if (
|
||||
!newModelId
|
||||
) {
|
||||
showError(
|
||||
"Model ID cannot be empty",
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// Check for duplicate
|
||||
const otherModelIds =
|
||||
selectedProvider?.models
|
||||
.filter(
|
||||
@@ -1231,15 +1314,20 @@ export function ModelConfigDialog({
|
||||
) ||
|
||||
[]
|
||||
if (
|
||||
newModelId &&
|
||||
otherModelIds.includes(
|
||||
newModelId,
|
||||
)
|
||||
) {
|
||||
toast.warning(
|
||||
`Model "${newModelId}" already exists. Please use a unique ID.`,
|
||||
showError(
|
||||
"This model ID already exists",
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// Clear error on valid blur
|
||||
setEditError(
|
||||
null,
|
||||
)
|
||||
}}
|
||||
className="flex-1 min-w-0 font-mono text-sm h-8 border-0 bg-transparent focus-visible:bg-background focus-visible:ring-1"
|
||||
/>
|
||||
@@ -1261,34 +1349,20 @@ export function ModelConfigDialog({
|
||||
{model.validated ===
|
||||
false &&
|
||||
model.validationError && (
|
||||
<p className="text-xs text-destructive px-3 pb-2 pl-14">
|
||||
<p className="text-[11px] text-destructive px-3 pb-2 pl-14">
|
||||
{
|
||||
model.validationError
|
||||
}
|
||||
</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,
|
||||
)
|
||||
{/* Show edit error inline */}
|
||||
{editError?.modelId ===
|
||||
model.id && (
|
||||
<p className="text-[11px] text-destructive px-3 pb-2 pl-14">
|
||||
{
|
||||
editError.message
|
||||
}
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
</div>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user