feat(knowledge): 笔记页实体可点击筛选 + chip 美化
- 实体改为按钮:点击即在左侧列表筛出提及该实体的知识(清空来源/状态筛选) - chip 加阴影/hover 上浮/类型 ring 标签/实体区 Sparkles 图标,更精致 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
b70ed38d51
commit
f6af9379b7
3 files changed
+42
-16
No files matched your search
@@ -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):
|
||||||
|
|||||||
Reference in new issue
Block a user