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
+152
-126
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)
|
||||||
|
|||||||
+111
-110
@@ -3,7 +3,7 @@
|
|||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Literal
|
from typing import Any, Literal
|
||||||
|
|
||||||
from pydantic import BaseModel, ConfigDict, Field
|
from pydantic import BaseModel, ConfigDict, Field
|
||||||
|
|
||||||
@@ -53,115 +53,116 @@ class McpServerConfig(BaseModel):
|
|||||||
model_config = ConfigDict(extra="allow", populate_by_name=True)
|
model_config = ConfigDict(extra="allow", populate_by_name=True)
|
||||||
|
|
||||||
|
|
||||||
class SkillDisplayTableColumn(BaseModel):
|
class SkillDisplayTableColumn(BaseModel):
|
||||||
"""A configured table column for structured skill result display."""
|
"""A configured table column for structured skill result display."""
|
||||||
|
|
||||||
label: str = Field(description="Column label shown in the frontend table")
|
label: str = Field(description="Column label shown in the frontend table")
|
||||||
field: str = Field(description="Field name or dot path read from each result item")
|
field: str = Field(description="Field name or dot path read from each result item")
|
||||||
width: str | None = Field(default=None, description="Optional CSS width for this column")
|
width: str | None = Field(default=None, description="Optional CSS width for this column")
|
||||||
model_config = ConfigDict(extra="allow", populate_by_name=True)
|
model_config = ConfigDict(extra="allow", populate_by_name=True)
|
||||||
|
|
||||||
|
|
||||||
class SkillDisplayResultSetConfig(BaseModel):
|
class SkillDisplayResultSetConfig(BaseModel):
|
||||||
"""Display adapter for one result array inside a skill response."""
|
"""Display adapter for one result array inside a skill response."""
|
||||||
|
|
||||||
key: str = Field(description="Stable key for this result set")
|
key: str = Field(description="Stable key for this result set")
|
||||||
label: str | None = Field(default=None, description="Human-readable result set label")
|
label: str | None = Field(default=None, description="Human-readable result set label")
|
||||||
enabled: bool = Field(default=True, description="Whether this result set is enabled")
|
enabled: bool = Field(default=True, description="Whether this result set is enabled")
|
||||||
mode: Literal["citation", "list", "table", "card", "custom", "none"] = Field(
|
mode: Literal["citation", "list", "table", "card", "custom", "none"] = Field(
|
||||||
default="citation",
|
default="citation",
|
||||||
description="How the frontend should render this result set",
|
description="How the frontend should render this result set",
|
||||||
)
|
)
|
||||||
result_path: str = Field(default="results", description="Dot path to the array of result items", alias="resultPath")
|
result_path: str = Field(default="results", description="Dot path to the array of result items", alias="resultPath")
|
||||||
title_field: str | None = Field(default=None, description="Field used as item title", alias="titleField")
|
title_field: str | None = Field(default=None, description="Field used as item title", alias="titleField")
|
||||||
summary_field: str | None = Field(default=None, description="Field used as short summary/snippet", alias="summaryField")
|
summary_field: str | None = Field(default=None, description="Field used as short summary/snippet", alias="summaryField")
|
||||||
content_field: str | None = Field(default=None, description="Field used as full item content", alias="contentField")
|
content_field: str | None = Field(default=None, description="Field used as full item content", alias="contentField")
|
||||||
source_field: str | None = Field(default=None, description="Field used as source label", alias="sourceField")
|
source_field: str | None = Field(default=None, description="Field used as source label", alias="sourceField")
|
||||||
author_field: str | None = Field(default=None, description="Field used as author/byline label", alias="authorField")
|
author_field: str | None = Field(default=None, description="Field used as author/byline label", alias="authorField")
|
||||||
type_field: str | None = Field(default=None, description="Field used as source/type badge", alias="typeField")
|
type_field: str | None = Field(default=None, description="Field used as source/type badge", alias="typeField")
|
||||||
time_field: str | None = Field(default=None, description="Field used as publication/update time", alias="timeField")
|
time_field: str | None = Field(default=None, description="Field used as publication/update time", alias="timeField")
|
||||||
score_field: str | None = Field(default=None, description="Field used as relevance score", alias="scoreField")
|
score_field: str | None = Field(default=None, description="Field used as relevance score", alias="scoreField")
|
||||||
url_field: str | None = Field(default=None, description="Field containing a direct URL", alias="urlField")
|
url_field: str | None = Field(default=None, description="Field containing a direct URL", alias="urlField")
|
||||||
id_field: str | None = Field(default=None, description="Field containing an ID for template URLs", alias="idField")
|
id_field: str | None = Field(default=None, description="Field containing an ID for template URLs", alias="idField")
|
||||||
link_strategy: Literal["auto", "url", "id"] = Field(
|
link_strategy: Literal["auto", "url", "id"] = Field(
|
||||||
default="auto",
|
default="auto",
|
||||||
description="Whether links use a URL field, an ID template, or automatic fallback",
|
description="Whether links use a URL field, an ID template, or automatic fallback",
|
||||||
alias="linkStrategy",
|
alias="linkStrategy",
|
||||||
)
|
)
|
||||||
link_template: str | None = Field(default=None, description="Template used when linkStrategy is id", alias="linkTemplate")
|
link_template: str | None = Field(default=None, description="Template used when linkStrategy is id", alias="linkTemplate")
|
||||||
citation_token: Literal["doubleBracket", "singleBracket"] = Field(
|
citation_token: Literal["doubleBracket", "singleBracket"] = Field(
|
||||||
default="singleBracket",
|
default="singleBracket",
|
||||||
description="Citation token style expected in model output",
|
description="Citation token style expected in model output",
|
||||||
alias="citationToken",
|
alias="citationToken",
|
||||||
)
|
)
|
||||||
max_items: int = Field(default=100, ge=1, le=100, description="Maximum displayed items", alias="maxItems")
|
max_items: int = Field(default=100, ge=1, le=100, description="Maximum displayed items", alias="maxItems")
|
||||||
custom_instruction: str = Field(
|
custom_instruction: str = Field(
|
||||||
default="",
|
default="",
|
||||||
description="User-authored instruction for custom rendering schema generation",
|
description="User-authored instruction for custom rendering schema generation",
|
||||||
alias="customInstruction",
|
alias="customInstruction",
|
||||||
)
|
)
|
||||||
table_columns: list[SkillDisplayTableColumn] = Field(
|
table_columns: list[SkillDisplayTableColumn] = Field(
|
||||||
default_factory=list,
|
default_factory=list,
|
||||||
description="Columns used when mode is table",
|
description="Columns used when mode is table",
|
||||||
alias="tableColumns",
|
alias="tableColumns",
|
||||||
)
|
)
|
||||||
model_config = ConfigDict(extra="allow", populate_by_name=True)
|
model_config = ConfigDict(extra="allow", populate_by_name=True)
|
||||||
|
|
||||||
|
|
||||||
class SkillDisplayConfig(BaseModel):
|
class SkillDisplayConfig(BaseModel):
|
||||||
"""Frontend display adapter for structured skill results."""
|
"""Frontend display adapter for structured skill results."""
|
||||||
|
|
||||||
enabled: bool = Field(default=False, description="Whether result display adaptation is enabled")
|
enabled: bool = Field(default=False, description="Whether result display adaptation is enabled")
|
||||||
mode: Literal["citation", "list", "table", "card", "custom", "none"] = Field(
|
mode: Literal["citation", "list", "table", "card", "custom", "none"] = Field(
|
||||||
default="none",
|
default="none",
|
||||||
description="How the frontend should render this skill's structured results",
|
description="How the frontend should render this skill's structured results",
|
||||||
)
|
)
|
||||||
result_path: str = Field(default="results", description="Dot path to the array of result items", alias="resultPath")
|
result_path: str = Field(default="results", description="Dot path to the array of result items", alias="resultPath")
|
||||||
title_field: str | None = Field(default=None, description="Field used as item title", alias="titleField")
|
title_field: str | None = Field(default=None, description="Field used as item title", alias="titleField")
|
||||||
summary_field: str | None = Field(default=None, description="Field used as short summary/snippet", alias="summaryField")
|
summary_field: str | None = Field(default=None, description="Field used as short summary/snippet", alias="summaryField")
|
||||||
content_field: str | None = Field(default=None, description="Field used as full item content", alias="contentField")
|
content_field: str | None = Field(default=None, description="Field used as full item content", alias="contentField")
|
||||||
source_field: str | None = Field(default=None, description="Field used as source label", alias="sourceField")
|
source_field: str | None = Field(default=None, description="Field used as source label", alias="sourceField")
|
||||||
author_field: str | None = Field(default=None, description="Field used as author/byline label", alias="authorField")
|
author_field: str | None = Field(default=None, description="Field used as author/byline label", alias="authorField")
|
||||||
type_field: str | None = Field(default=None, description="Field used as source/type badge", alias="typeField")
|
type_field: str | None = Field(default=None, description="Field used as source/type badge", alias="typeField")
|
||||||
time_field: str | None = Field(default=None, description="Field used as publication/update time", alias="timeField")
|
time_field: str | None = Field(default=None, description="Field used as publication/update time", alias="timeField")
|
||||||
score_field: str | None = Field(default=None, description="Field used as relevance score", alias="scoreField")
|
score_field: str | None = Field(default=None, description="Field used as relevance score", alias="scoreField")
|
||||||
url_field: str | None = Field(default=None, description="Field containing a direct URL", alias="urlField")
|
url_field: str | None = Field(default=None, description="Field containing a direct URL", alias="urlField")
|
||||||
id_field: str | None = Field(default=None, description="Field containing an ID for template URLs", alias="idField")
|
id_field: str | None = Field(default=None, description="Field containing an ID for template URLs", alias="idField")
|
||||||
link_strategy: Literal["auto", "url", "id"] = Field(
|
link_strategy: Literal["auto", "url", "id"] = Field(
|
||||||
default="auto",
|
default="auto",
|
||||||
description="Whether links use a URL field, an ID template, or automatic fallback",
|
description="Whether links use a URL field, an ID template, or automatic fallback",
|
||||||
alias="linkStrategy",
|
alias="linkStrategy",
|
||||||
)
|
)
|
||||||
link_template: str | None = Field(default=None, description="Template used when linkStrategy is id", alias="linkTemplate")
|
link_template: str | None = Field(default=None, description="Template used when linkStrategy is id", alias="linkTemplate")
|
||||||
citation_token: Literal["doubleBracket", "singleBracket"] = Field(
|
citation_token: Literal["doubleBracket", "singleBracket"] = Field(
|
||||||
default="doubleBracket",
|
default="doubleBracket",
|
||||||
description="Citation token style expected in model output",
|
description="Citation token style expected in model output",
|
||||||
alias="citationToken",
|
alias="citationToken",
|
||||||
)
|
)
|
||||||
max_items: int = Field(default=8, ge=1, le=100, description="Maximum displayed items", alias="maxItems")
|
max_items: int = Field(default=8, ge=1, le=100, description="Maximum displayed items", alias="maxItems")
|
||||||
custom_instruction: str = Field(
|
custom_instruction: str = Field(
|
||||||
default="",
|
default="",
|
||||||
description="User-authored instruction for custom rendering schema generation",
|
description="User-authored instruction for custom rendering schema generation",
|
||||||
alias="customInstruction",
|
alias="customInstruction",
|
||||||
)
|
)
|
||||||
table_columns: list[SkillDisplayTableColumn] = Field(
|
table_columns: list[SkillDisplayTableColumn] = Field(
|
||||||
default_factory=list,
|
default_factory=list,
|
||||||
description="Columns used when mode is table",
|
description="Columns used when mode is table",
|
||||||
alias="tableColumns",
|
alias="tableColumns",
|
||||||
)
|
)
|
||||||
result_sets: list[SkillDisplayResultSetConfig] = Field(
|
result_sets: list[SkillDisplayResultSetConfig] = Field(
|
||||||
default_factory=list,
|
default_factory=list,
|
||||||
description="Optional per-result-array display adapters",
|
description="Optional per-result-array display adapters",
|
||||||
alias="resultSets",
|
alias="resultSets",
|
||||||
)
|
)
|
||||||
model_config = ConfigDict(extra="allow", populate_by_name=True)
|
model_config = ConfigDict(extra="allow", populate_by_name=True)
|
||||||
|
|
||||||
|
|
||||||
class SkillStateConfig(BaseModel):
|
class SkillStateConfig(BaseModel):
|
||||||
"""Configuration for a single skill's state."""
|
"""Configuration for a single skill's state."""
|
||||||
|
|
||||||
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