diff --git a/internal/http/provider_models.go b/internal/http/provider_models.go index 7801d376..ed2fa4fc 100644 --- a/internal/http/provider_models.go +++ b/internal/http/provider_models.go @@ -195,6 +195,7 @@ func minimaxModels() []ModelInfo { // Chat / text {ID: "MiniMax-Text-01", Name: "MiniMax Text 01"}, {ID: "MiniMax-M1", Name: "MiniMax M1"}, + {ID: "MiniMax-M2.7", Name: "MiniMax M2.7"}, {ID: "MiniMax-M2.5", Name: "MiniMax M2.5"}, // Image generation {ID: "image-01", Name: "Image 01"}, diff --git a/ui/web/src/components/shared/provider-model-select.tsx b/ui/web/src/components/shared/provider-model-select.tsx index e0c61e00..51df31cf 100644 --- a/ui/web/src/components/shared/provider-model-select.tsx +++ b/ui/web/src/components/shared/provider-model-select.tsx @@ -148,6 +148,8 @@ export function ProviderModelSelect({ onChange={onModelChange} options={models.map((m) => ({ value: m.id, label: m.name }))} placeholder={modelsLoading ? t("loadingModels") : (modelPlaceholder ?? t("enterOrSelectModel"))} + allowCustom + customLabel={t("useCustomModel")} /> {shouldShowVerify && ( diff --git a/ui/web/src/components/shared/skill-name-select.tsx b/ui/web/src/components/shared/skill-name-select.tsx index 71cb82f6..ff39f537 100644 --- a/ui/web/src/components/shared/skill-name-select.tsx +++ b/ui/web/src/components/shared/skill-name-select.tsx @@ -122,7 +122,7 @@ export function SkillNameSelect({ onFocus={() => setOpen(true)} onKeyDown={handleKeyDown} placeholder={value.length === 0 ? (placeholder ?? t("selectOrTypeSkills")) : ""} - className="placeholder:text-muted-foreground min-w-[80px] flex-1 bg-transparent py-0.5 text-sm outline-none" + className="placeholder:text-muted-foreground min-w-[80px] flex-1 bg-transparent py-0.5 text-base md:text-sm outline-none" /> { if (input.trim()) addTag(input); }} placeholder={value.length === 0 ? (placeholder ?? t("typeAndPressEnter")) : ""} - className="placeholder:text-muted-foreground min-w-[80px] flex-1 bg-transparent py-0.5 text-sm outline-none" + className="placeholder:text-muted-foreground min-w-[80px] flex-1 bg-transparent py-0.5 text-base md:text-sm outline-none" /> ); diff --git a/ui/web/src/components/shared/tool-name-select.tsx b/ui/web/src/components/shared/tool-name-select.tsx index 2f02094f..4bd88391 100644 --- a/ui/web/src/components/shared/tool-name-select.tsx +++ b/ui/web/src/components/shared/tool-name-select.tsx @@ -150,7 +150,7 @@ export function ToolNameSelect({ onFocus={() => setOpen(true)} onKeyDown={handleKeyDown} placeholder={value.length === 0 ? (placeholder ?? t("selectOrTypeTools")) : ""} - className="placeholder:text-muted-foreground min-w-[80px] flex-1 bg-transparent py-0.5 text-sm outline-none" + className="placeholder:text-muted-foreground min-w-[80px] flex-1 bg-transparent py-0.5 text-base md:text-sm outline-none" /> ; + /** Allow typing custom values not in the options list. Shows a hint in the dropdown. */ + allowCustom?: boolean; + /** Label for the custom value hint (default: "Use custom:"). */ + customLabel?: string; } export function Combobox({ @@ -25,9 +29,15 @@ export function Combobox({ placeholder, className, portalContainer, + allowCustom, + customLabel = "Use custom:", }: ComboboxProps) { const [open, setOpen] = React.useState(false); const [search, setSearch] = React.useState(""); + // Track whether user actively typed since last focus — when false, show all options + const inputDirtyRef = React.useRef(false); + const [inputDirty, setInputDirty] = React.useState(false); + const inputRef = React.useRef(null); const containerRef = React.useRef(null); const dropdownRef = React.useRef(null); const [dropdownStyle, setDropdownStyle] = React.useState({}); @@ -48,6 +58,8 @@ export function Combobox({ (!dropdownRef.current || !dropdownRef.current.contains(target)) ) { setOpen(false); + setInputDirty(false); + inputDirtyRef.current = false; } }; document.addEventListener("mousedown", handler); @@ -92,14 +104,23 @@ export function Combobox({ } }, [open, search, resolvedPortal]); + // When not dirty (just focused), show all options. When dirty, filter by search. const filtered = React.useMemo(() => { - if (!search) return options; + if (!inputDirty || !search) return options; const q = search.toLowerCase(); return options.filter( (o) => o.value.toLowerCase().includes(q) || (o.label && o.label.toLowerCase().includes(q)), ); + }, [options, search, inputDirty]); + + // Check if typed value is a custom value (not matching any option exactly) + const isCustomValue = React.useMemo(() => { + if (!search.trim()) return false; + return !options.some( + (o) => o.value === search || o.label === search, + ); }, [options, search]); const handleSelect = (val: string) => { @@ -107,16 +128,33 @@ export function Combobox({ const match = options.find((o) => o.value === val); setSearch(match?.label || val); setOpen(false); + setInputDirty(false); + inputDirtyRef.current = false; }; const handleInputChange = (e: React.ChangeEvent) => { const val = e.target.value; setSearch(val); onChange(val); + if (!inputDirtyRef.current) { + inputDirtyRef.current = true; + setInputDirty(true); + } if (!open && options.length > 0) setOpen(true); }; - const dropdownContent = open && filtered.length > 0 && ( + const handleFocus = () => { + // Reset dirty state on focus — shows all options initially + inputDirtyRef.current = false; + setInputDirty(false); + if (options.length > 0) setOpen(true); + // Select all text so user can start typing to replace + requestAnimationFrame(() => inputRef.current?.select()); + }; + + const showCustomHint = allowCustom && inputDirty && isCustomValue && search.trim(); + + const dropdownContent = open && (filtered.length > 0 || showCustomHint) && (
))} + {showCustomHint && ( + + )}
); return (
options.length > 0 && setOpen(true)} + onFocus={handleFocus} placeholder={placeholder} className={cn( "border-input placeholder:text-muted-foreground dark:bg-input/30 h-9 w-full rounded-md border bg-transparent px-3 py-1 pr-8 text-base md:text-sm shadow-xs outline-none transition-[color,box-shadow]", diff --git a/ui/web/src/i18n/locales/en/common.json b/ui/web/src/i18n/locales/en/common.json index f9b8aeaa..05af8f9d 100644 --- a/ui/web/src/i18n/locales/en/common.json +++ b/ui/web/src/i18n/locales/en/common.json @@ -99,6 +99,7 @@ "model": "Model", "selectProvider": "Select provider", "enterOrSelectModel": "Enter or select model", + "useCustomModel": "Use custom:", "providerTip": "LLM provider name. Must match a configured provider.", "modelTip": "Model ID to use.", "check": "Check", diff --git a/ui/web/src/i18n/locales/vi/common.json b/ui/web/src/i18n/locales/vi/common.json index b12f83a9..329183e7 100644 --- a/ui/web/src/i18n/locales/vi/common.json +++ b/ui/web/src/i18n/locales/vi/common.json @@ -45,6 +45,7 @@ "manage": "Quản lý", "messages": "tin nhắn", "model": "Model", + "useCustomModel": "Dùng tuỳ chỉnh:", "modelTip": "ID model để sử dụng.", "modelVerified": "Model đã được xác minh", "name": "Tên", diff --git a/ui/web/src/i18n/locales/zh/common.json b/ui/web/src/i18n/locales/zh/common.json index be6adcb1..e7426a67 100644 --- a/ui/web/src/i18n/locales/zh/common.json +++ b/ui/web/src/i18n/locales/zh/common.json @@ -45,6 +45,7 @@ "manage": "管理", "messages": "条消息", "model": "模型", + "useCustomModel": "使用自定义:", "modelTip": "要使用的模型 ID。", "modelVerified": "模型已验证", "name": "名称", diff --git a/ui/web/src/pages/agents/agent-detail/agent-instances-tab.tsx b/ui/web/src/pages/agents/agent-detail/agent-instances-tab.tsx index 682e7fa0..0575ae1c 100644 --- a/ui/web/src/pages/agents/agent-detail/agent-instances-tab.tsx +++ b/ui/web/src/pages/agents/agent-detail/agent-instances-tab.tsx @@ -202,7 +202,7 @@ function ContactSearchBox({ existingIDs, onSelect }: { existingIDs: Set; onChange={(e) => { setSearch(e.target.value); setOpen(true); }} onFocus={() => search.length >= 2 && setOpen(true)} placeholder={t("instances.searchContacts")} - className="h-8 w-full rounded-md border bg-transparent pl-7 pr-2 text-xs placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring" + className="h-8 w-full rounded-md border bg-transparent pl-7 pr-2 text-base md:text-xs placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring" />
{open && search.length >= 2 && filtered.length > 0 && createPortal( diff --git a/ui/web/src/pages/agents/agent-detail/file-sections/contact-insert-search.tsx b/ui/web/src/pages/agents/agent-detail/file-sections/contact-insert-search.tsx index 0da352f0..ed40f754 100644 --- a/ui/web/src/pages/agents/agent-detail/file-sections/contact-insert-search.tsx +++ b/ui/web/src/pages/agents/agent-detail/file-sections/contact-insert-search.tsx @@ -72,7 +72,7 @@ export function ContactInsertSearch({ onInsert }: ContactInsertSearchProps) { onChange={(e) => { setSearch(e.target.value); setOpen(true); }} onFocus={() => search.length >= 2 && setOpen(true)} placeholder={t("files.insertContact")} - className="h-8 w-full max-w-sm rounded-md border bg-transparent pl-7 pr-2 text-xs placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring" + className="h-8 w-full max-w-sm rounded-md border bg-transparent pl-7 pr-2 text-base md:text-xs placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring" /> {open && search.length >= 2 && contacts.length > 0 && createPortal( diff --git a/ui/web/src/pages/events/events-page.tsx b/ui/web/src/pages/events/events-page.tsx index dcf1df2e..16706f29 100644 --- a/ui/web/src/pages/events/events-page.tsx +++ b/ui/web/src/pages/events/events-page.tsx @@ -200,7 +200,7 @@ export function EventsPage() { setUserFilter(e.target.value)} - className="h-7 rounded-md border bg-background px-2 text-xs" + className="h-7 rounded-md border bg-background px-2 text-base md:text-xs" > {uniqueUsers.map((uid) => ( @@ -229,7 +229,7 @@ export function EventsPage() { { setAgentId(e.target.value); setUserIdFilter(""); }} - className="mt-2 h-9 rounded-md border bg-background px-3 text-sm" + className="mt-2 h-9 rounded-md border bg-background px-3 text-base md:text-sm" > {agents.map((a) => ( diff --git a/ui/web/src/pages/login/pairing-form.tsx b/ui/web/src/pages/login/pairing-form.tsx index 2223f62d..10fd3904 100644 --- a/ui/web/src/pages/login/pairing-form.tsx +++ b/ui/web/src/pages/login/pairing-form.tsx @@ -169,7 +169,7 @@ export function PairingForm({ onApproved }: PairingFormProps) { value={userId} onChange={(e) => setUserId(e.target.value)} placeholder={t("pairing.userIdPlaceholder")} - className="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring" + className="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base md:text-sm shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring" autoFocus /> diff --git a/ui/web/src/pages/login/token-form.tsx b/ui/web/src/pages/login/token-form.tsx index 56279c31..2036c632 100644 --- a/ui/web/src/pages/login/token-form.tsx +++ b/ui/web/src/pages/login/token-form.tsx @@ -59,7 +59,7 @@ export function TokenForm({ onSubmit }: TokenFormProps) { value={userId} onChange={(e) => { setUserId(e.target.value); setError(null); }} placeholder={t("token.userIdPlaceholder")} - className="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring" + className="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base md:text-sm shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring" autoFocus disabled={connecting} /> @@ -75,7 +75,7 @@ export function TokenForm({ onSubmit }: TokenFormProps) { value={token} onChange={(e) => { setToken(e.target.value); setError(null); }} placeholder={t("token.tokenPlaceholder")} - className="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring" + className="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base md:text-sm shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring" disabled={connecting} /> diff --git a/ui/web/src/pages/logs/logs-page.tsx b/ui/web/src/pages/logs/logs-page.tsx index 1fb56c99..0e132fcb 100644 --- a/ui/web/src/pages/logs/logs-page.tsx +++ b/ui/web/src/pages/logs/logs-page.tsx @@ -70,7 +70,7 @@ export function LogsPage() { setCommand(e.target.value)} placeholder="npx -y @modelcontextprotocol/server-everything" className="font-mono text-sm" /> + setCommand(e.target.value)} placeholder="npx -y @modelcontextprotocol/server-everything" className="font-mono" />
- setArgs(e.target.value)} placeholder={t("form.argsPlaceholder")} className="font-mono text-sm" /> + setArgs(e.target.value)} placeholder={t("form.argsPlaceholder")} className="font-mono" />
) : ( <>
- setUrl(e.target.value)} placeholder="http://localhost:3001/sse" className="font-mono text-sm" /> + setUrl(e.target.value)} placeholder="http://localhost:3001/sse" className="font-mono" />
@@ -237,7 +237,7 @@ export function MCPFormDialog({ open, onOpenChange, server, onSubmit, onTest }:
mcp_ - setToolPrefix(e.target.value.replace(/[^a-z0-9_]/g, ""))} placeholder={name.replace(/-/g, "_") || "auto"} className="rounded-l-none font-mono text-sm" /> + setToolPrefix(e.target.value.replace(/[^a-z0-9_]/g, ""))} placeholder={name.replace(/-/g, "_") || "auto"} className="rounded-l-none font-mono" />

{t("form.toolPrefixHint")} Tools: mcp_{prefix}__{tool}

diff --git a/ui/web/src/pages/mcp/mcp-grants-dialog.tsx b/ui/web/src/pages/mcp/mcp-grants-dialog.tsx index 61608479..d3b357f0 100644 --- a/ui/web/src/pages/mcp/mcp-grants-dialog.tsx +++ b/ui/web/src/pages/mcp/mcp-grants-dialog.tsx @@ -408,7 +408,7 @@ function ToolMultiSelect({ onFocus={() => setOpen(true)} onKeyDown={handleKeyDown} placeholder={value.length === 0 ? placeholder : ""} - className="placeholder:text-muted-foreground min-w-[80px] flex-1 bg-transparent py-0.5 text-sm outline-none" + className="placeholder:text-muted-foreground min-w-[80px] flex-1 bg-transparent py-0.5 text-base md:text-sm outline-none" /> setSelectedAgentId(e.target.value)} - className="h-9 rounded-md border bg-background px-3 text-sm" + className="h-9 rounded-md border bg-background px-3 text-base md:text-sm" > {agents.map((a) => ( @@ -158,7 +158,7 @@ export function MemoryCreateDialog({ open, onOpenChange, agentId: parentAgentId, setTitleDraft(e.target.value)} onKeyDown={(e) => {