feat(knowledge): 笔记页实体可点击筛选 + chip 美化

- 实体改为按钮:点击即在左侧列表筛出提及该实体的知识(清空来源/状态筛选)
- chip 加阴影/hover 上浮/类型 ring 标签/实体区 Sparkles 图标,更精致

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
AdministratorandClaude Opus 4.8 committed 2026-06-09 19:48:20 +08:00
1 parent b70ed38d51
commit f6af9379b7
3 files changed
+42 -16

No files matched your search

+30 -10
View File
@@ -9,6 +9,7 @@ import {
RefreshCw, RefreshCw,
Save, Save,
Search, Search,
Sparkles,
Trash2, Trash2,
X, X,
} from "lucide-react"; } from "lucide-react";
@@ -415,7 +416,13 @@ function VaultPageDialog({
); );
} }
function NoteDetail({ noteId }: { noteId: string }) { function NoteDetail({
noteId,
onEntityClick,
}: {
noteId: string;
onEntityClick?: (name: string) => void;
}) {
const navigate = useNavigate(); const navigate = useNavigate();
const { data: note, isLoading } = useKnowledgeNote(noteId); const { data: note, isLoading } = useKnowledgeNote(noteId);
const { data: wikiLinks } = useNoteWikilinks(noteId); const { data: wikiLinks } = useNoteWikilinks(noteId);
@@ -578,22 +585,28 @@ function NoteDetail({ noteId }: { noteId: string }) {
{note.entities && note.entities.length > 0 ? ( {note.entities && note.entities.length > 0 ? (
<div className="mb-4 flex flex-wrap items-center gap-1.5"> <div className="mb-4 flex flex-wrap items-center gap-1.5">
<span className="text-muted-foreground mr-0.5 text-xs">实体</span> <span className="text-muted-foreground mr-0.5 inline-flex items-center gap-1 text-xs">
<Sparkles className="size-3" /> 实体
</span>
{note.entities.map((ent) => { {note.entities.map((ent) => {
const meta = entityMeta(ent.entity_type); const meta = entityMeta(ent.entity_type);
return ( return (
<span <button
type="button"
key={`${ent.name}-${ent.entity_type ?? ""}`} key={`${ent.name}-${ent.entity_type ?? ""}`}
onClick={() => onEntityClick?.(ent.name)}
title={`筛选提及「${ent.name}」的知识`}
className={cn( className={cn(
"inline-flex items-center gap-1 rounded-full border px-2 py-0.5 text-xs font-medium", "group inline-flex cursor-pointer items-center gap-1.5 rounded-full border px-2.5 py-1 text-xs font-medium shadow-sm transition-all hover:-translate-y-px hover:shadow",
meta.className, meta.className,
)} )}
title={meta.label}
> >
<span className="size-1.5 rounded-full bg-current opacity-60" /> <span className="size-1.5 shrink-0 rounded-full bg-current opacity-70 transition-transform group-hover:scale-125" />
{ent.name} <span>{ent.name}</span>
<span className="opacity-60">{meta.label}</span> <span className="rounded-sm px-1 text-[10px] opacity-70 ring-1 ring-current ring-inset">
</span> {meta.label}
</span>
</button>
); );
})} })}
</div> </div>
@@ -779,7 +792,14 @@ export default function KnowledgeBasePage() {
<div className="min-h-0"> <div className="min-h-0">
{noteId ? ( {noteId ? (
<NoteDetail noteId={noteId} /> <NoteDetail
noteId={noteId}
onEntityClick={(name) => {
setKeyword(name);
setSourceType("all");
setStatus("all");
}}
/>
) : ( ) : (
<div className="text-muted-foreground flex h-full items-center justify-center text-sm"> <div className="text-muted-foreground flex h-full items-center justify-center text-sm">
从左侧选择一条知识查看详情 从左侧选择一条知识查看详情
@@ -149,6 +149,7 @@ class SkillResponse(BaseModel):
"""Response model for skill information.""" """Response model for skill information."""
name: str = Field(..., description="Name of the skill") name: str = Field(..., description="Name of the skill")
name_zh: str | None = Field(default=None, description="用户配置或大模型生成的中文名称")
description: str = Field(..., description="Description of what the skill does") description: str = Field(..., description="Description of what the skill does")
license: str | None = Field(None, description="License information") license: str | None = Field(None, description="License information")
category: SkillCategory = Field(..., description="Category of the skill (public or custom)") category: SkillCategory = Field(..., description="Category of the skill (public or custom)")
@@ -313,16 +314,19 @@ def _skill_to_response(
detail = ownership.get("detail") detail = ownership.get("detail")
display = None if display_override is _UNSET else display_override display = None if display_override is _UNSET else display_override
if display_override is _UNSET: name_zh: str | None = None
try: try:
skill_config = get_extensions_config().skills.get(skill.name) skill_config = get_extensions_config().skills.get(skill.name)
if skill_config and skill_config.display: if skill_config:
if display_override is _UNSET and skill_config.display:
display = skill_config.display.model_dump(by_alias=True) display = skill_config.display.model_dump(by_alias=True)
except Exception: name_zh = skill_config.name_zh or None
logger.debug("Failed to read display adapter for %s", skill.name, exc_info=True) except Exception:
logger.debug("Failed to read extensions config for %s", skill.name, exc_info=True)
return SkillResponse( return SkillResponse(
name=skill.name, name=skill.name,
name_zh=name_zh,
description=skill.description, description=skill.description,
license=skill.license, license=skill.license,
category=skill.category, category=skill.category,
@@ -2142,6 +2146,7 @@ async def update_skill(skill_name: str, request: SkillUpdateRequest, config: App
extensions_config.skills[skill_name] = SkillStateConfig( extensions_config.skills[skill_name] = SkillStateConfig(
enabled=request.enabled, enabled=request.enabled,
display=current_skill_config.display if current_skill_config else None, display=current_skill_config.display if current_skill_config else None,
name_zh=current_skill_config.name_zh if current_skill_config else None,
) )
config_data = _dump_extensions_config(extensions_config) config_data = _dump_extensions_config(extensions_config)
@@ -162,6 +162,7 @@ class SkillStateConfig(BaseModel):
enabled: bool = Field(default=True, description="Whether this skill is enabled") enabled: bool = Field(default=True, description="Whether this skill is enabled")
display: SkillDisplayConfig | None = Field(default=None, description="Optional frontend display adapter") display: SkillDisplayConfig | None = Field(default=None, description="Optional frontend display adapter")
name_zh: str | None = Field(default=None, description="用户配置或大模型生成的中文名称(为空表示沿用技能英文名)")
class ExtensionsConfig(BaseModel): class ExtensionsConfig(BaseModel):