feat(web): show disabled reasons in tooltips when buttons are disabled (#2430)
* feat(web): show disabled reasons in tooltips when buttons are disabled - Add disabled reason tooltips for model card actions (set default, delete) - Add disabled reason tooltips for marketplace skill card install button - Add disabled reason display for chat input when disabled - Add internationalization support for all disabled reasons (en/zh) - Model card: Show specific reasons when set-default or delete buttons are disabled - Marketplace skill card: Show specific reasons when install button is disabled - Chat composer: Show reason text below input when input is disabled * fix: show disabled action reasons via tooltips * fix(web): restore accessible labels for model action tooltips
This commit is contained in:
parent
c0fadc5918
commit
bf6d4fd997
5 changed files with 187 additions and 46 deletions
|
|
@ -18,6 +18,11 @@ import {
|
||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@/components/ui/card"
|
} from "@/components/ui/card"
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/ui/tooltip"
|
||||||
|
|
||||||
export function MarketSkillCard({
|
export function MarketSkillCard({
|
||||||
result,
|
result,
|
||||||
|
|
@ -36,6 +41,17 @@ export function MarketSkillCard({
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
const installDisabledReason = (() => {
|
||||||
|
if (installPending)
|
||||||
|
return t("pages.agent.skills.marketplace_installDisabled.installing")
|
||||||
|
if (result.installed)
|
||||||
|
return t("pages.agent.skills.marketplace_installDisabled.installed")
|
||||||
|
if (!canInstall)
|
||||||
|
return t("pages.agent.skills.marketplace_installDisabled.cannotInstall")
|
||||||
|
return t("pages.agent.skills.marketplace_install_action")
|
||||||
|
})()
|
||||||
|
const installDisabled = !canInstall || result.installed || installPending
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
className="group border-border/40 bg-card/40 hover:border-border/80 hover:bg-card relative overflow-hidden transition-all hover:shadow-md"
|
className="group border-border/40 bg-card/40 hover:border-border/80 hover:bg-card relative overflow-hidden transition-all hover:shadow-md"
|
||||||
|
|
@ -86,24 +102,34 @@ export function MarketSkillCard({
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex shrink-0 flex-col items-end gap-2">
|
<div className="flex shrink-0 flex-col items-end gap-2">
|
||||||
<Button
|
<Tooltip delayDuration={installDisabled ? 0 : 700}>
|
||||||
size="sm"
|
<TooltipTrigger asChild>
|
||||||
variant={result.installed ? "secondary" : "default"}
|
<span
|
||||||
className="shadow-sm transition-all"
|
className={installDisabled ? "cursor-not-allowed" : undefined}
|
||||||
disabled={!canInstall || result.installed || installPending}
|
tabIndex={installDisabled ? 0 : undefined}
|
||||||
onClick={onInstall}
|
>
|
||||||
>
|
<Button
|
||||||
{installPending ? (
|
size="sm"
|
||||||
<IconLoader2 className="size-4 animate-spin" />
|
variant={result.installed ? "secondary" : "default"}
|
||||||
) : result.installed ? (
|
className="shadow-sm transition-all"
|
||||||
<IconCheck className="size-4" />
|
disabled={installDisabled}
|
||||||
) : (
|
onClick={onInstall}
|
||||||
<IconPlus className="size-4" />
|
>
|
||||||
)}
|
{installPending ? (
|
||||||
{result.installed
|
<IconLoader2 className="size-4 animate-spin" />
|
||||||
? t("pages.agent.skills.marketplace_installed")
|
) : result.installed ? (
|
||||||
: t("pages.agent.skills.marketplace_install_action")}
|
<IconCheck className="size-4" />
|
||||||
</Button>
|
) : (
|
||||||
|
<IconPlus className="size-4" />
|
||||||
|
)}
|
||||||
|
{result.installed
|
||||||
|
? t("pages.agent.skills.marketplace_installed")
|
||||||
|
: t("pages.agent.skills.marketplace_install_action")}
|
||||||
|
</Button>
|
||||||
|
</span>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>{installDisabledReason}</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
{result.installed && installedSkill ? (
|
{result.installed && installedSkill ? (
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,12 @@ export function ChatComposer({
|
||||||
? t("chat.placeholder")
|
? t("chat.placeholder")
|
||||||
: t(`chat.disabledPlaceholder.${inputDisabledReason}`)
|
: t(`chat.disabledPlaceholder.${inputDisabledReason}`)
|
||||||
|
|
||||||
|
const inputDisabledReason = (() => {
|
||||||
|
if (!isConnected) return t("chat.inputDisabled.notConnected")
|
||||||
|
if (!hasDefaultModel) return t("chat.inputDisabled.noModel")
|
||||||
|
return null
|
||||||
|
})()
|
||||||
|
|
||||||
const handleKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) => {
|
const handleKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) => {
|
||||||
if (e.nativeEvent.isComposing) return
|
if (e.nativeEvent.isComposing) return
|
||||||
if (e.key === "Enter" && !e.shiftKey) {
|
if (e.key === "Enter" && !e.shiftKey) {
|
||||||
|
|
@ -89,6 +95,7 @@ export function ChatComposer({
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
disabled={!canInput}
|
disabled={!canInput}
|
||||||
|
title={inputDisabledReason || undefined}
|
||||||
className={cn(
|
className={cn(
|
||||||
"placeholder:text-muted-foreground/50 max-h-[200px] min-h-[60px] resize-none border-0 bg-transparent px-2 py-1 text-[15px] shadow-none transition-colors focus-visible:ring-0 focus-visible:outline-none dark:bg-transparent",
|
"placeholder:text-muted-foreground/50 max-h-[200px] min-h-[60px] resize-none border-0 bg-transparent px-2 py-1 text-[15px] shadow-none transition-colors focus-visible:ring-0 focus-visible:outline-none dark:bg-transparent",
|
||||||
!canInput && "cursor-not-allowed",
|
!canInput && "cursor-not-allowed",
|
||||||
|
|
@ -96,6 +103,11 @@ export function ChatComposer({
|
||||||
minRows={1}
|
minRows={1}
|
||||||
maxRows={8}
|
maxRows={8}
|
||||||
/>
|
/>
|
||||||
|
{!canInput && inputDisabledReason && (
|
||||||
|
<div className="px-3 py-1 text-xs text-muted-foreground">
|
||||||
|
{inputDisabledReason}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="mt-2 flex items-center justify-between px-1">
|
<div className="mt-2 flex items-center justify-between px-1">
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,11 @@ import { useTranslation } from "react-i18next"
|
||||||
|
|
||||||
import type { ModelInfo } from "@/api/models"
|
import type { ModelInfo } from "@/api/models"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/ui/tooltip"
|
||||||
|
|
||||||
interface ModelCardProps {
|
interface ModelCardProps {
|
||||||
model: ModelInfo
|
model: ModelInfo
|
||||||
|
|
@ -33,6 +38,23 @@ export function ModelCard({
|
||||||
const canSetDefault =
|
const canSetDefault =
|
||||||
model.available && !model.is_default && !model.is_virtual
|
model.available && !model.is_default && !model.is_virtual
|
||||||
|
|
||||||
|
const setDefaultLabel = t("models.action.setDefault")
|
||||||
|
const setDefaultDisabledReason = (() => {
|
||||||
|
if (settingDefault) return t("models.action.setDefaultDisabled.setting")
|
||||||
|
if (!model.available)
|
||||||
|
return t("models.action.setDefaultDisabled.unavailable")
|
||||||
|
if (model.is_default) return t("models.action.setDefaultDisabled.isDefault")
|
||||||
|
if (model.is_virtual) return t("models.action.setDefaultDisabled.isVirtual")
|
||||||
|
return setDefaultLabel
|
||||||
|
})()
|
||||||
|
|
||||||
|
const editLabel = t("models.action.edit")
|
||||||
|
const deleteLabel = t("models.action.delete")
|
||||||
|
const deleteDisabledReason = model.is_default
|
||||||
|
? t("models.action.deleteDisabled.isDefault")
|
||||||
|
: deleteLabel
|
||||||
|
const deleteDisabled = model.is_default
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={[
|
className={[
|
||||||
|
|
@ -81,40 +103,85 @@ export function ModelCard({
|
||||||
<IconStarFilled className="size-3.5" />
|
<IconStarFilled className="size-3.5" />
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<Button
|
<Tooltip delayDuration={!canSetDefault || settingDefault ? 0 : 700}>
|
||||||
variant="ghost"
|
<TooltipTrigger asChild>
|
||||||
size="icon-sm"
|
<span
|
||||||
onClick={() => onSetDefault(model)}
|
className={
|
||||||
disabled={settingDefault || !canSetDefault}
|
!canSetDefault || settingDefault
|
||||||
title={t("models.action.setDefault")}
|
? "cursor-not-allowed"
|
||||||
>
|
: undefined
|
||||||
{settingDefault ? (
|
}
|
||||||
<IconLoader2 className="size-3.5 animate-spin" />
|
tabIndex={!canSetDefault || settingDefault ? 0 : undefined}
|
||||||
) : (
|
role={!canSetDefault || settingDefault ? "button" : undefined}
|
||||||
<IconStar className="size-3.5" />
|
aria-disabled={
|
||||||
)}
|
!canSetDefault || settingDefault ? true : undefined
|
||||||
</Button>
|
}
|
||||||
|
aria-label={
|
||||||
|
!canSetDefault || settingDefault
|
||||||
|
? setDefaultLabel
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
title={
|
||||||
|
!canSetDefault || settingDefault
|
||||||
|
? setDefaultLabel
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon-sm"
|
||||||
|
onClick={() => onSetDefault(model)}
|
||||||
|
disabled={settingDefault || !canSetDefault}
|
||||||
|
aria-label={setDefaultLabel}
|
||||||
|
title={setDefaultLabel}
|
||||||
|
>
|
||||||
|
{settingDefault ? (
|
||||||
|
<IconLoader2 className="size-3.5 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<IconStar className="size-3.5" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</span>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>{setDefaultDisabledReason}</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon-sm"
|
size="icon-sm"
|
||||||
onClick={() => onEdit(model)}
|
onClick={() => onEdit(model)}
|
||||||
title={t("models.action.edit")}
|
aria-label={editLabel}
|
||||||
|
title={editLabel}
|
||||||
>
|
>
|
||||||
<IconEdit className="size-3.5" />
|
<IconEdit className="size-3.5" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Tooltip delayDuration={deleteDisabled ? 0 : 700}>
|
||||||
variant="ghost"
|
<TooltipTrigger asChild>
|
||||||
size="icon-sm"
|
<span
|
||||||
onClick={() => onDelete(model)}
|
className={deleteDisabled ? "cursor-not-allowed" : undefined}
|
||||||
disabled={model.is_default}
|
tabIndex={deleteDisabled ? 0 : undefined}
|
||||||
title={t("models.action.delete")}
|
role={deleteDisabled ? "button" : undefined}
|
||||||
className="text-muted-foreground hover:text-destructive hover:bg-destructive/10"
|
aria-disabled={deleteDisabled ? true : undefined}
|
||||||
>
|
aria-label={deleteDisabled ? deleteLabel : undefined}
|
||||||
<IconTrash className="size-3.5" />
|
title={deleteDisabled ? deleteLabel : undefined}
|
||||||
</Button>
|
>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon-sm"
|
||||||
|
onClick={() => onDelete(model)}
|
||||||
|
disabled={deleteDisabled}
|
||||||
|
aria-label={deleteLabel}
|
||||||
|
title={deleteLabel}
|
||||||
|
className="text-muted-foreground hover:text-destructive hover:bg-destructive/10"
|
||||||
|
>
|
||||||
|
<IconTrash className="size-3.5" />
|
||||||
|
</Button>
|
||||||
|
</span>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>{deleteDisabledReason}</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -68,6 +68,10 @@
|
||||||
"deleteSession": "Delete session",
|
"deleteSession": "Delete session",
|
||||||
"messagesCount": "{{count}} messages",
|
"messagesCount": "{{count}} messages",
|
||||||
"noModel": "Select model",
|
"noModel": "Select model",
|
||||||
|
"inputDisabled": {
|
||||||
|
"notConnected": "Gateway is not running. Start it to chat.",
|
||||||
|
"noModel": "No default model configured. Go to Models page to set one."
|
||||||
|
},
|
||||||
"attachImage": "Add images",
|
"attachImage": "Add images",
|
||||||
"removeImage": "Remove image",
|
"removeImage": "Remove image",
|
||||||
"uploadedImage": "Uploaded image",
|
"uploadedImage": "Uploaded image",
|
||||||
|
|
@ -212,7 +216,16 @@
|
||||||
"action": {
|
"action": {
|
||||||
"edit": "Edit API key",
|
"edit": "Edit API key",
|
||||||
"setDefault": "Set as default",
|
"setDefault": "Set as default",
|
||||||
"delete": "Delete model"
|
"delete": "Delete model",
|
||||||
|
"setDefaultDisabled": {
|
||||||
|
"setting": "Setting as default...",
|
||||||
|
"unavailable": "Cannot set unavailable model as default",
|
||||||
|
"isDefault": "Already the default model",
|
||||||
|
"isVirtual": "Cannot set virtual model as default"
|
||||||
|
},
|
||||||
|
"deleteDisabled": {
|
||||||
|
"isDefault": "Cannot delete the default model"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"defaultOnSave": {
|
"defaultOnSave": {
|
||||||
"label": "Default Model",
|
"label": "Default Model",
|
||||||
|
|
@ -500,6 +513,11 @@
|
||||||
"version": "Installed Version",
|
"version": "Installed Version",
|
||||||
"lines": "Line Count",
|
"lines": "Line Count",
|
||||||
"characters": "Character Count"
|
"characters": "Character Count"
|
||||||
|
},
|
||||||
|
"marketplace_installDisabled": {
|
||||||
|
"installing": "Installing...",
|
||||||
|
"installed": "Already installed",
|
||||||
|
"cannotInstall": "Cannot install: related tool is not enabled"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"tools": {
|
"tools": {
|
||||||
|
|
@ -668,4 +686,4 @@
|
||||||
"description": "Need more help? Click the documentation button in the top right corner to view detailed guides and configuration docs."
|
"description": "Need more help? Click the documentation button in the top right corner to view detailed guides and configuration docs."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -68,6 +68,10 @@
|
||||||
"deleteSession": "删除会话",
|
"deleteSession": "删除会话",
|
||||||
"messagesCount": "{{count}} 条消息",
|
"messagesCount": "{{count}} 条消息",
|
||||||
"noModel": "选择模型",
|
"noModel": "选择模型",
|
||||||
|
"inputDisabled": {
|
||||||
|
"notConnected": "服务未运行,请先启动以进行对话。",
|
||||||
|
"noModel": "未设置默认模型,请前往模型页面进行配置。"
|
||||||
|
},
|
||||||
"attachImage": "添加图片",
|
"attachImage": "添加图片",
|
||||||
"removeImage": "移除图片",
|
"removeImage": "移除图片",
|
||||||
"uploadedImage": "已上传图片",
|
"uploadedImage": "已上传图片",
|
||||||
|
|
@ -212,7 +216,16 @@
|
||||||
"action": {
|
"action": {
|
||||||
"edit": "编辑 API Key",
|
"edit": "编辑 API Key",
|
||||||
"setDefault": "设为默认",
|
"setDefault": "设为默认",
|
||||||
"delete": "删除模型"
|
"delete": "删除模型",
|
||||||
|
"setDefaultDisabled": {
|
||||||
|
"setting": "正在设为默认...",
|
||||||
|
"unavailable": "无法将不可用的模型设为默认",
|
||||||
|
"isDefault": "该模型已是默认模型",
|
||||||
|
"isVirtual": "无法将虚拟模型设为默认"
|
||||||
|
},
|
||||||
|
"deleteDisabled": {
|
||||||
|
"isDefault": "无法删除默认模型"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"defaultOnSave": {
|
"defaultOnSave": {
|
||||||
"label": "默认模型",
|
"label": "默认模型",
|
||||||
|
|
@ -500,6 +513,11 @@
|
||||||
"version": "已安装版本",
|
"version": "已安装版本",
|
||||||
"lines": "行数",
|
"lines": "行数",
|
||||||
"characters": "字符数"
|
"characters": "字符数"
|
||||||
|
},
|
||||||
|
"marketplace_installDisabled": {
|
||||||
|
"installing": "正在安装...",
|
||||||
|
"installed": "已安装",
|
||||||
|
"cannotInstall": "无法安装:相关工具未启用"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"tools": {
|
"tools": {
|
||||||
|
|
@ -668,4 +686,4 @@
|
||||||
"description": "需要更多帮助?点击右上角的文档按钮,查看详细的使用文档和配置指南。"
|
"description": "需要更多帮助?点击右上角的文档按钮,查看详细的使用文档和配置指南。"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue