feat: improve agent skill selection
This commit is contained in:
1 parent
0dd93421c4
commit
637be00946
3 files changed
+57
-103
No files matched your search
@@ -45,11 +45,32 @@ function skillLabel(skill: Skill) {
|
||||
|
||||
function matchesQuery(skill: Skill, q: string) {
|
||||
if (!q) return true;
|
||||
return (
|
||||
skill.name.toLowerCase().includes(q) ||
|
||||
(skill.name_zh ?? "").toLowerCase().includes(q) ||
|
||||
(skill.description ?? "").toLowerCase().includes(q)
|
||||
);
|
||||
const haystack = [
|
||||
skill.name,
|
||||
skill.name_zh,
|
||||
skill.description,
|
||||
skill.detail,
|
||||
skill.category,
|
||||
skill.license,
|
||||
skill.source,
|
||||
skill.state,
|
||||
skill.owner_username,
|
||||
skill.created_at,
|
||||
skill.last_activity_at,
|
||||
skill.use_count != null ? `使用 ${skill.use_count}` : null,
|
||||
skill.view_count != null ? `查看 ${skill.view_count}` : null,
|
||||
skill.patch_count != null ? `变迁 ${skill.patch_count}` : null,
|
||||
...(skill.tags ?? []).flatMap((tag) => [
|
||||
tag.name,
|
||||
tag.description,
|
||||
tag.color,
|
||||
tag.usage_count != null ? `标签使用 ${tag.usage_count}` : null,
|
||||
]),
|
||||
]
|
||||
.filter((value): value is string => typeof value === "string" && value.length > 0)
|
||||
.join(" ")
|
||||
.toLowerCase();
|
||||
return haystack.includes(q);
|
||||
}
|
||||
|
||||
export function SkillTransferSelector({
|
||||
@@ -159,7 +180,7 @@ export function SkillTransferSelector({
|
||||
<div className="relative flex-1">
|
||||
<SearchIcon className="text-muted-foreground absolute left-2 top-1/2 size-3.5 -translate-y-1/2" />
|
||||
<Input
|
||||
placeholder="搜索全部技能(中英文名称、描述)…"
|
||||
placeholder="搜索技能名称、描述、分类、标签、变迁..."
|
||||
value={search}
|
||||
onChange={(e) => {
|
||||
setSearch(e.currentTarget.value);
|
||||
@@ -239,7 +260,7 @@ export function SkillTransferSelector({
|
||||
{/* Two-column transfer panes */}
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<SkillPane
|
||||
title="可用技能"
|
||||
title="未勾选技能"
|
||||
count={availableSkills.length}
|
||||
items={availableView}
|
||||
variant="available"
|
||||
@@ -248,10 +269,10 @@ export function SkillTransferSelector({
|
||||
onPageChange={setAvailablePage}
|
||||
onPick={add}
|
||||
onViewDetail={onViewDetail}
|
||||
emptyHint={query || category !== ALL_CATEGORIES ? "没有匹配的技能" : "已全部启用"}
|
||||
emptyHint={query || category !== ALL_CATEGORIES ? "没有匹配的技能" : "已全部勾选"}
|
||||
/>
|
||||
<SkillPane
|
||||
title="已启用技能"
|
||||
title="已勾选技能"
|
||||
count={selectedSkills.length}
|
||||
items={selectedView}
|
||||
variant="selected"
|
||||
@@ -260,7 +281,7 @@ export function SkillTransferSelector({
|
||||
onPageChange={setSelectedPage}
|
||||
onPick={remove}
|
||||
onViewDetail={onViewDetail}
|
||||
emptyHint={query ? "没有匹配的已选技能" : "尚未启用任何技能"}
|
||||
emptyHint={query ? "没有匹配的已选技能" : "尚未勾选任何技能"}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -470,11 +470,11 @@ function SkillSettingsList({ onClose }: { onClose?: () => void }) {
|
||||
</Button>
|
||||
</div>
|
||||
</header>
|
||||
<TagSearchBar
|
||||
targetType="skill"
|
||||
onSearchChange={setSearch}
|
||||
tagIds={tagIds}
|
||||
onTagIdsChange={setTagIds}
|
||||
<TagSearchBar
|
||||
targetType="skill"
|
||||
onSearchChange={setSearch}
|
||||
tagIds={tagIds}
|
||||
onTagIdsChange={setTagIds}
|
||||
actions={
|
||||
<div className="flex items-center gap-2">
|
||||
<ViewModeToggle value={viewMode} onChange={setViewMode} />
|
||||
@@ -497,11 +497,16 @@ function SkillSettingsList({ onClose }: { onClose?: () => void }) {
|
||||
<SelectItem value="name_desc">名称 Z-A</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
{publisher && (
|
||||
<div className="flex items-center gap-2">
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
{!isLoading && !error && (
|
||||
<div className="text-muted-foreground flex justify-end text-sm tabular-nums">
|
||||
共 {orderedSkills.length} 个技能
|
||||
</div>
|
||||
)}
|
||||
{publisher && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-muted-foreground text-xs">筛选发布者:</span>
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -3,9 +3,7 @@
|
||||
import {
|
||||
ArrowLeftIcon,
|
||||
CheckCircleIcon,
|
||||
InfoIcon,
|
||||
SaveIcon,
|
||||
SearchIcon,
|
||||
} from "lucide-react";
|
||||
import { useParams, useRouter } from "next/navigation";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
@@ -38,6 +36,7 @@ import {
|
||||
type SetupAgentApprovalPayload,
|
||||
} from "@/components/workspace/messages";
|
||||
import { ThreadContext } from "@/components/workspace/messages/context";
|
||||
import { SkillTransferSelector } from "@/components/workspace/agents/skill-transfer-selector";
|
||||
import { SkillDetailDialog } from "@/components/workspace/skills/skill-detail-dialog";
|
||||
import { getAPIClient } from "@/core/api";
|
||||
import type { Agent } from "@/core/agents";
|
||||
@@ -51,7 +50,7 @@ import { getStoredAuth } from "@/core/auth";
|
||||
import { useLiveAccount } from "@/core/auth/use-live-account";
|
||||
import { useI18n } from "@/core/i18n/hooks";
|
||||
import { useModels } from "@/core/models/hooks";
|
||||
import { isAgentSkillCandidate, type Skill } from "@/core/skills";
|
||||
import { isAgentSkillCandidate } from "@/core/skills";
|
||||
import { useSkills } from "@/core/skills/hooks";
|
||||
import { useThreadStream } from "@/core/threads/hooks";
|
||||
import { uuid } from "@/core/utils/uuid";
|
||||
@@ -98,7 +97,6 @@ export default function NewAgentPage() {
|
||||
const [selectedSkills, setSelectedSkills] = useState<string[]>([]);
|
||||
const [isSkillSelectionTouched, setIsSkillSelectionTouched] = useState(false);
|
||||
const [detailSkillName, setDetailSkillName] = useState<string | null>(null);
|
||||
const [skillSearch, setSkillSearch] = useState("");
|
||||
// 恢复已有 bootstrap 会话时该 thread 已存在,无需再 create(置为 true,跳过创建)。
|
||||
const [threadPrepared, setThreadPrepared] = useState(!!resumeThreadId);
|
||||
const { skills, isLoading: isSkillsLoading } = useSkills();
|
||||
@@ -123,17 +121,6 @@ export default function NewAgentPage() {
|
||||
[skills, storedAuth],
|
||||
);
|
||||
|
||||
const filteredSkills = useMemo(() => {
|
||||
const q = skillSearch.trim().toLowerCase();
|
||||
if (!q) return enabledSkills;
|
||||
return enabledSkills.filter(
|
||||
(skill) =>
|
||||
skill.name.toLowerCase().includes(q) ||
|
||||
(skill.name_zh ?? "").toLowerCase().includes(q) ||
|
||||
(skill.description ?? "").toLowerCase().includes(q),
|
||||
);
|
||||
}, [enabledSkills, skillSearch]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isSkillSelectionTouched) {
|
||||
return;
|
||||
@@ -380,14 +367,10 @@ export default function NewAgentPage() {
|
||||
[handleChatSubmit],
|
||||
);
|
||||
|
||||
const toggleSkill = useCallback((skill: Skill) => {
|
||||
const handleSkillSelectionChange = useCallback((next: string[]) => {
|
||||
setIsSkillSelectionTouched(true);
|
||||
setFormError("");
|
||||
setSelectedSkills((prev) =>
|
||||
prev.includes(skill.name)
|
||||
? prev.filter((name) => name !== skill.name)
|
||||
: [...prev, skill.name],
|
||||
);
|
||||
setSelectedSkills(next);
|
||||
}, []);
|
||||
|
||||
const busy = isCreatingAgent || isSavingDraft || thread.isLoading;
|
||||
@@ -624,68 +607,13 @@ export default function NewAgentPage() {
|
||||
已选 {selectedSkills.length} 个
|
||||
</span>
|
||||
</div>
|
||||
{enabledSkills.length > 0 && (
|
||||
<div className="relative">
|
||||
<SearchIcon className="text-muted-foreground absolute left-2 top-1/2 size-3.5 -translate-y-1/2" />
|
||||
<Input
|
||||
placeholder="搜索技能名称或描述…"
|
||||
value={skillSearch}
|
||||
onChange={(e) => setSkillSearch(e.currentTarget.value)}
|
||||
className="h-8 pl-7 text-xs"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="max-h-[240px] overflow-y-auto rounded-md border p-2 dark:border-[#1B477E] dark:bg-[#022248]">
|
||||
{isSkillsLoading ? (
|
||||
<p className="text-muted-foreground text-sm">技能加载中...</p>
|
||||
) : enabledSkills.length === 0 ? (
|
||||
<p className="text-muted-foreground text-sm">
|
||||
当前没有可用技能。
|
||||
</p>
|
||||
) : filteredSkills.length === 0 ? (
|
||||
<p className="text-muted-foreground text-sm">
|
||||
没有匹配的技能。
|
||||
</p>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 gap-2 sm:grid-cols-2">
|
||||
{filteredSkills.map((skill) => {
|
||||
const checked = selectedSkills.includes(skill.name);
|
||||
return (
|
||||
<div
|
||||
key={skill.name}
|
||||
className="flex w-full min-w-0 items-center"
|
||||
>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant={checked ? "default" : "outline"}
|
||||
onClick={() => toggleSkill(skill)}
|
||||
title={skill.description}
|
||||
className={cn(
|
||||
"h-8 min-w-0 flex-1 rounded-r-none",
|
||||
!checked && "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
<span className="truncate">
|
||||
{skill.name_zh?.trim() ? skill.name_zh : skill.name}
|
||||
</span>
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
size="icon"
|
||||
variant={checked ? "default" : "outline"}
|
||||
onClick={() => setDetailSkillName(skill.name)}
|
||||
title={t.settings.skills.viewDetailAction}
|
||||
className="h-8 w-7 rounded-l-none border-l-0"
|
||||
>
|
||||
<InfoIcon className="size-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<SkillTransferSelector
|
||||
skills={enabledSkills}
|
||||
selected={selectedSkills}
|
||||
onChange={handleSkillSelectionChange}
|
||||
isLoading={isSkillsLoading}
|
||||
onViewDetail={setDetailSkillName}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<SkillDetailDialog
|
||||
|
||||
Reference in new issue
Block a user