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) {
|
function matchesQuery(skill: Skill, q: string) {
|
||||||
if (!q) return true;
|
if (!q) return true;
|
||||||
return (
|
const haystack = [
|
||||||
skill.name.toLowerCase().includes(q) ||
|
skill.name,
|
||||||
(skill.name_zh ?? "").toLowerCase().includes(q) ||
|
skill.name_zh,
|
||||||
(skill.description ?? "").toLowerCase().includes(q)
|
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({
|
export function SkillTransferSelector({
|
||||||
@@ -159,7 +180,7 @@ export function SkillTransferSelector({
|
|||||||
<div className="relative flex-1">
|
<div className="relative flex-1">
|
||||||
<SearchIcon className="text-muted-foreground absolute left-2 top-1/2 size-3.5 -translate-y-1/2" />
|
<SearchIcon className="text-muted-foreground absolute left-2 top-1/2 size-3.5 -translate-y-1/2" />
|
||||||
<Input
|
<Input
|
||||||
placeholder="搜索全部技能(中英文名称、描述)…"
|
placeholder="搜索技能名称、描述、分类、标签、变迁..."
|
||||||
value={search}
|
value={search}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setSearch(e.currentTarget.value);
|
setSearch(e.currentTarget.value);
|
||||||
@@ -239,7 +260,7 @@ export function SkillTransferSelector({
|
|||||||
{/* Two-column transfer panes */}
|
{/* Two-column transfer panes */}
|
||||||
<div className="grid grid-cols-2 gap-3">
|
<div className="grid grid-cols-2 gap-3">
|
||||||
<SkillPane
|
<SkillPane
|
||||||
title="可用技能"
|
title="未勾选技能"
|
||||||
count={availableSkills.length}
|
count={availableSkills.length}
|
||||||
items={availableView}
|
items={availableView}
|
||||||
variant="available"
|
variant="available"
|
||||||
@@ -248,10 +269,10 @@ export function SkillTransferSelector({
|
|||||||
onPageChange={setAvailablePage}
|
onPageChange={setAvailablePage}
|
||||||
onPick={add}
|
onPick={add}
|
||||||
onViewDetail={onViewDetail}
|
onViewDetail={onViewDetail}
|
||||||
emptyHint={query || category !== ALL_CATEGORIES ? "没有匹配的技能" : "已全部启用"}
|
emptyHint={query || category !== ALL_CATEGORIES ? "没有匹配的技能" : "已全部勾选"}
|
||||||
/>
|
/>
|
||||||
<SkillPane
|
<SkillPane
|
||||||
title="已启用技能"
|
title="已勾选技能"
|
||||||
count={selectedSkills.length}
|
count={selectedSkills.length}
|
||||||
items={selectedView}
|
items={selectedView}
|
||||||
variant="selected"
|
variant="selected"
|
||||||
@@ -260,7 +281,7 @@ export function SkillTransferSelector({
|
|||||||
onPageChange={setSelectedPage}
|
onPageChange={setSelectedPage}
|
||||||
onPick={remove}
|
onPick={remove}
|
||||||
onViewDetail={onViewDetail}
|
onViewDetail={onViewDetail}
|
||||||
emptyHint={query ? "没有匹配的已选技能" : "尚未启用任何技能"}
|
emptyHint={query ? "没有匹配的已选技能" : "尚未勾选任何技能"}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -470,11 +470,11 @@ function SkillSettingsList({ onClose }: { onClose?: () => void }) {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<TagSearchBar
|
<TagSearchBar
|
||||||
targetType="skill"
|
targetType="skill"
|
||||||
onSearchChange={setSearch}
|
onSearchChange={setSearch}
|
||||||
tagIds={tagIds}
|
tagIds={tagIds}
|
||||||
onTagIdsChange={setTagIds}
|
onTagIdsChange={setTagIds}
|
||||||
actions={
|
actions={
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<ViewModeToggle value={viewMode} onChange={setViewMode} />
|
<ViewModeToggle value={viewMode} onChange={setViewMode} />
|
||||||
@@ -497,11 +497,16 @@ function SkillSettingsList({ onClose }: { onClose?: () => void }) {
|
|||||||
<SelectItem value="name_desc">名称 Z-A</SelectItem>
|
<SelectItem value="name_desc">名称 Z-A</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
{publisher && (
|
{!isLoading && !error && (
|
||||||
<div className="flex items-center gap-2">
|
<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>
|
<span className="text-muted-foreground text-xs">筛选发布者:</span>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -3,9 +3,7 @@
|
|||||||
import {
|
import {
|
||||||
ArrowLeftIcon,
|
ArrowLeftIcon,
|
||||||
CheckCircleIcon,
|
CheckCircleIcon,
|
||||||
InfoIcon,
|
|
||||||
SaveIcon,
|
SaveIcon,
|
||||||
SearchIcon,
|
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useParams, useRouter } from "next/navigation";
|
import { useParams, useRouter } from "next/navigation";
|
||||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
@@ -38,6 +36,7 @@ import {
|
|||||||
type SetupAgentApprovalPayload,
|
type SetupAgentApprovalPayload,
|
||||||
} from "@/components/workspace/messages";
|
} from "@/components/workspace/messages";
|
||||||
import { ThreadContext } from "@/components/workspace/messages/context";
|
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 { SkillDetailDialog } from "@/components/workspace/skills/skill-detail-dialog";
|
||||||
import { getAPIClient } from "@/core/api";
|
import { getAPIClient } from "@/core/api";
|
||||||
import type { Agent } from "@/core/agents";
|
import type { Agent } from "@/core/agents";
|
||||||
@@ -51,7 +50,7 @@ import { getStoredAuth } from "@/core/auth";
|
|||||||
import { useLiveAccount } from "@/core/auth/use-live-account";
|
import { useLiveAccount } from "@/core/auth/use-live-account";
|
||||||
import { useI18n } from "@/core/i18n/hooks";
|
import { useI18n } from "@/core/i18n/hooks";
|
||||||
import { useModels } from "@/core/models/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 { useSkills } from "@/core/skills/hooks";
|
||||||
import { useThreadStream } from "@/core/threads/hooks";
|
import { useThreadStream } from "@/core/threads/hooks";
|
||||||
import { uuid } from "@/core/utils/uuid";
|
import { uuid } from "@/core/utils/uuid";
|
||||||
@@ -98,7 +97,6 @@ export default function NewAgentPage() {
|
|||||||
const [selectedSkills, setSelectedSkills] = useState<string[]>([]);
|
const [selectedSkills, setSelectedSkills] = useState<string[]>([]);
|
||||||
const [isSkillSelectionTouched, setIsSkillSelectionTouched] = useState(false);
|
const [isSkillSelectionTouched, setIsSkillSelectionTouched] = useState(false);
|
||||||
const [detailSkillName, setDetailSkillName] = useState<string | null>(null);
|
const [detailSkillName, setDetailSkillName] = useState<string | null>(null);
|
||||||
const [skillSearch, setSkillSearch] = useState("");
|
|
||||||
// 恢复已有 bootstrap 会话时该 thread 已存在,无需再 create(置为 true,跳过创建)。
|
// 恢复已有 bootstrap 会话时该 thread 已存在,无需再 create(置为 true,跳过创建)。
|
||||||
const [threadPrepared, setThreadPrepared] = useState(!!resumeThreadId);
|
const [threadPrepared, setThreadPrepared] = useState(!!resumeThreadId);
|
||||||
const { skills, isLoading: isSkillsLoading } = useSkills();
|
const { skills, isLoading: isSkillsLoading } = useSkills();
|
||||||
@@ -123,17 +121,6 @@ export default function NewAgentPage() {
|
|||||||
[skills, storedAuth],
|
[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(() => {
|
useEffect(() => {
|
||||||
if (isSkillSelectionTouched) {
|
if (isSkillSelectionTouched) {
|
||||||
return;
|
return;
|
||||||
@@ -380,14 +367,10 @@ export default function NewAgentPage() {
|
|||||||
[handleChatSubmit],
|
[handleChatSubmit],
|
||||||
);
|
);
|
||||||
|
|
||||||
const toggleSkill = useCallback((skill: Skill) => {
|
const handleSkillSelectionChange = useCallback((next: string[]) => {
|
||||||
setIsSkillSelectionTouched(true);
|
setIsSkillSelectionTouched(true);
|
||||||
setFormError("");
|
setFormError("");
|
||||||
setSelectedSkills((prev) =>
|
setSelectedSkills(next);
|
||||||
prev.includes(skill.name)
|
|
||||||
? prev.filter((name) => name !== skill.name)
|
|
||||||
: [...prev, skill.name],
|
|
||||||
);
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const busy = isCreatingAgent || isSavingDraft || thread.isLoading;
|
const busy = isCreatingAgent || isSavingDraft || thread.isLoading;
|
||||||
@@ -624,68 +607,13 @@ export default function NewAgentPage() {
|
|||||||
已选 {selectedSkills.length} 个
|
已选 {selectedSkills.length} 个
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{enabledSkills.length > 0 && (
|
<SkillTransferSelector
|
||||||
<div className="relative">
|
skills={enabledSkills}
|
||||||
<SearchIcon className="text-muted-foreground absolute left-2 top-1/2 size-3.5 -translate-y-1/2" />
|
selected={selectedSkills}
|
||||||
<Input
|
onChange={handleSkillSelectionChange}
|
||||||
placeholder="搜索技能名称或描述…"
|
isLoading={isSkillsLoading}
|
||||||
value={skillSearch}
|
onViewDetail={setDetailSkillName}
|
||||||
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>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<SkillDetailDialog
|
<SkillDetailDialog
|
||||||
|
|||||||
Reference in new issue
Block a user