diff --git a/frontend-web/src/components/workspace/agents/agent-card.tsx b/frontend-web/src/components/workspace/agents/agent-card.tsx index cb4e510c..c60e2084 100644 --- a/frontend-web/src/components/workspace/agents/agent-card.tsx +++ b/frontend-web/src/components/workspace/agents/agent-card.tsx @@ -6,9 +6,9 @@ import { GlobeIcon, InfoIcon, LockIcon, - MessageSquareIcon, - PinIcon, - ShieldIcon, + MessageSquareIcon, + PinIcon, + ShieldIcon, SparklesIcon, SquarePenIcon, StarIcon, @@ -54,11 +54,11 @@ import { useAgent, useCreateAgent, useDeleteAgent, - useSetAgentFeatured, - useSetAgentPrivate, - useToggleAgentFavorite, - useToggleAgentPin, - useUpdateAgent, + useSetAgentFeatured, + useSetAgentPrivate, + useToggleAgentFavorite, + useToggleAgentPin, + useUpdateAgent, } from "@/core/agents"; import { getAgent } from "@/core/agents/api"; import type { Agent } from "@/core/agents"; @@ -70,6 +70,9 @@ import { cn } from "@/lib/utils"; interface AgentCardProps { agent: Agent; + /** "card" (default) renders the full gallery card; "list" renders a compact + * single-row layout for the list view. All dialogs/actions are shared. */ + layout?: "card" | "list"; } interface BadgeItem { @@ -181,16 +184,17 @@ function ClampedBadgeRow({ items }: { items: BadgeItem[] }) { ); } -export function AgentCard({ agent }: AgentCardProps) { +export function AgentCard({ agent, layout = "card" }: AgentCardProps) { + const isList = layout === "list"; const { t } = useI18n(); const router = useRouter(); const deleteAgent = useDeleteAgent(); const updateAgent = useUpdateAgent(); const createAgent = useCreateAgent(); const setFeatured = useSetAgentFeatured(); - const setPrivate = useSetAgentPrivate(); - const toggleFavorite = useToggleAgentFavorite(); - const togglePin = useToggleAgentPin(); + const setPrivate = useSetAgentPrivate(); + const toggleFavorite = useToggleAgentFavorite(); + const togglePin = useToggleAgentPin(); const { skills, isLoading: isSkillsLoading } = useSkills(); const [deleteOpen, setDeleteOpen] = useState(false); const [editOpen, setEditOpen] = useState(false); @@ -228,8 +232,8 @@ export function AgentCard({ agent }: AgentCardProps) { // Favoriting collects a square agent into "我的"; pointless for one you // already own, so only offer it on built-in / others' agents. const canFavorite = !isOwned; - const isFavorite = !!agent.is_favorite; - const isPinned = !!agent.is_pinned; + const isFavorite = !!agent.is_favorite; + const isPinned = !!agent.is_pinned; useEffect(() => { if (!editOpen) { @@ -383,7 +387,7 @@ export function AgentCard({ agent }: AgentCardProps) { } } - async function handleToggleFavorite() { + async function handleToggleFavorite() { const next = !isFavorite; try { await toggleFavorite.mutateAsync({ id: agent.id, favorite: next }); @@ -391,17 +395,17 @@ export function AgentCard({ agent }: AgentCardProps) { } catch (err) { toast.error(err instanceof Error ? err.message : String(err)); } - } - - async function handleTogglePin() { - const next = !isPinned; - try { - await togglePin.mutateAsync({ id: agent.id, pinned: next }); - toast.success(next ? "已置顶" : "已取消置顶"); - } catch (err) { - toast.error(err instanceof Error ? err.message : String(err)); - } - } + } + + async function handleTogglePin() { + const next = !isPinned; + try { + await togglePin.mutateAsync({ id: agent.id, pinned: next }); + toast.success(next ? "已置顶" : "已取消置顶"); + } catch (err) { + toast.error(err instanceof Error ? err.message : String(err)); + } + } async function handleToggleFeatured() { // One-click on/off — ordering is handled by the dedicated 管理 Agent 列表 @@ -434,274 +438,328 @@ export function AgentCard({ agent }: AgentCardProps) { const enabledSkills = skills.filter((skill) => skill.enabled); - return ( + // Status badges (内置 / 我的 / 来自他人 / 已发布 / 私密 / 首页推荐) — shared by + // both the card header and the compact list row. + const statusBadges = ( <> - - -
-
-
- -
-
- - {desensitize(agent.name)} - - {agent.model && ( - - {desensitize(agent.model)} - - )} -
+ {isBuiltin && ( + + 内置 + + )} + {isOwned && ( + + 我的 + + )} + {isFromOthers && ( + + 来自他人 + + )} + {isOwned && agent.published && !agent.is_private && ( + + + 已发布 + + )} + {agent.is_private && ( + + + 私密 + + )} + {agent.featured_order != null && ( + + + 首页推荐 + + )} + + ); + + const skillBadgeItems = [ + ...(agent.tool_groups ?? []).map((group) => ({ + key: `tg:${group}`, + node: ( + + {desensitize(group)} + + ), + })), + ...(agent.skills ?? []).map((skill) => ({ + key: `sk:${skill}`, + node: ( + { + e.stopPropagation(); + setDetailSkillName(skill); + }} + > + {desensitize(skill)} + + ), + })), + ]; + const hasSkillsOrTools = + (agent.tool_groups?.length ?? agent.skills?.length ?? 0) > 0; + + const showActionRow = + canEdit || canDelete || isOwned || isAdmin || canFavorite; + + // The icon action buttons, shared by both layouts. + const actionButtons = ( + <> + {canFavorite && ( + + )} + + {isAdmin && ( + + )} + {isAdmin && ( + + )} + {isOwned && ( + + )} + {(isOwned || isAdmin) && ( + + )} + + {canEdit && ( + + )} + {canDelete && ( + + )} + + ); + + const chatButton = ( + + ); + + const cardLayout = ( + + +
+
+
+
-
- {isBuiltin && ( - - 内置 - - )} - {isOwned && ( - - 我的 - - )} - {isFromOthers && ( - - 来自他人 - - )} - {isOwned && agent.published && !agent.is_private && ( - - - 已发布 - - )} - {agent.is_private && ( - - - 私密 - - )} - {agent.featured_order != null && ( - - - 首页推荐 +
+ + {desensitize(agent.name)} + + {agent.model && ( + + {desensitize(agent.model)} )}
- {agent.description && ( - - {desensitize(agent.description)} - - )} - - - {(agent.tags?.length ?? 0) > 0 && ( - - - +
+ {statusBadges} +
+
+ {agent.description && ( + + {desensitize(agent.description)} + )} + - {(agent.tool_groups?.length ?? agent.skills?.length ?? 0) > 0 && ( - - ({ - key: `tg:${group}`, - node: ( - - {desensitize(group)} - - ), - })), - ...(agent.skills ?? []).map((skill) => ({ - key: `sk:${skill}`, - node: ( - { - e.stopPropagation(); - setDetailSkillName(skill); - }} - > - {desensitize(skill)} - - ), - })), - ]} - /> - + {(agent.tags?.length ?? 0) > 0 && ( + + + + )} + + {hasSkillsOrTools && ( + + + + )} + + + {showActionRow && ( +
+ {actionButtons} +
)} + {chatButton} +
+ + ); - - {(canEdit || canDelete || isOwned || isAdmin || canFavorite) && ( -
- {canFavorite && ( - - )} - - {isAdmin && ( - - )} - {isAdmin && ( - - )} - {isOwned && ( - - )} - {(isOwned || isAdmin) && ( - - )} - - {canEdit && ( - - )} - {canDelete && ( - - )} -
- )} - -
- + const listLayout = ( +
+
+ +
+
+
+ + {desensitize(agent.name)} + +
{statusBadges}
+
+ {agent.description && ( +

+ {desensitize(agent.description)} +

+ )} +
+
+ {showActionRow && actionButtons} + {chatButton} +
+
+ ); + + return ( + <> + {isList ? listLayout : cardLayout} diff --git a/frontend-web/src/components/workspace/agents/agent-gallery.tsx b/frontend-web/src/components/workspace/agents/agent-gallery.tsx index 9a7d94b8..3be26cef 100644 --- a/frontend-web/src/components/workspace/agents/agent-gallery.tsx +++ b/frontend-web/src/components/workspace/agents/agent-gallery.tsx @@ -11,10 +11,11 @@ import { Pagination } from "@/components/ui/pagination"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { TagSearchBar } from "@/components/workspace/tags/tag-search-bar"; +import { ViewModeToggle } from "@/components/workspace/view-mode-toggle"; import { useAgentsPaginated, usePrivateAgentsPaginated } from "@/core/agents"; import type { Agent, AgentSort } from "@/core/agents"; import { useI18n } from "@/core/i18n/hooks"; -import { usePrivateSquareStatus } from "@/core/system-settings/hooks"; +import { usePrivateSquareStatus, useViewMode } from "@/core/system-settings/hooks"; import { AgentCard } from "./agent-card"; @@ -28,6 +29,7 @@ const PRIVATE_SQUARE_PASSWORD_KEY = "deer-flow.private-square-password"; export function AgentGallery() { const { t } = useI18n(); const router = useRouter(); + const [viewMode, setViewMode] = useViewMode("agent"); const [activeTab, setActiveTab] = useState<"all" | "mine" | "private">( "mine", ); @@ -125,13 +127,20 @@ export function AgentGallery() { setPrivatePassword(null); }; - const renderGrid = (list: Agent[]) => ( -
- {list.map((agent) => ( - - ))} -
- ); + const renderGrid = (list: Agent[]) => + viewMode === "list" ? ( +
+ {list.map((agent) => ( + + ))} +
+ ) : ( +
+ {list.map((agent) => ( + + ))} +
+ ); /** Loading / empty / (grid + pagination) for one paginated tab. */ const renderListBody = (opts: { @@ -308,6 +317,7 @@ export function AgentGallery() { 共 {activeTotal} 个 + {activeTab === "private" && privatePassword && ( + +

+ 当前系统默认:{current === "card" ? "卡片" : "列表"} +

+
+ ))} +
+ + + )} + void }) { const [tagIds, setTagIds] = useState([]); const [sort, setSort] = useState("time_desc"); const [page, setPage] = useState(1); + const [viewMode, setViewMode] = useViewMode("skill"); const reconcileMutation = useReconcileCustomSkills(); // Name + tag filtering is resolved server-side via the /api/skills query. const { skills, isLoading, error } = useSkills({ search, tagIds, sort }); @@ -335,25 +338,28 @@ function SkillSettingsList({ onClose }: { onClose?: () => void }) { tagIds={tagIds} onTagIdsChange={setTagIds} actions={ - +
+ + +
} /> {isLoading && ( @@ -364,13 +370,20 @@ function SkillSettingsList({ onClose }: { onClose?: () => void }) { )} {!isLoading && !error && filteredSkills.length > 0 && ( -
+
{pagedSkills.map((skill) => ( ))}
@@ -427,11 +440,15 @@ export function SkillRow({ skill, currentUserId, isAdmin, + layout = "row", }: { skill: Skill; currentUserId: string | null; isAdmin: boolean; + /** "row" = full-width horizontal item (default); "card" = grid card cell. */ + layout?: "row" | "card"; }) { + const isCard = layout === "card"; const { t } = useI18n(); const setPublished = useSetSkillPublished(); const setPinned = useSetSkillPinned(); @@ -529,207 +546,239 @@ export function SkillRow({ } } + const badgeGroup = ( +
+ + {desensitize(skill.name)} + + {isBuiltin && ( + + {t.settings.skills.badgeBuiltin} + + )} + {isOwned && ( + + {t.settings.skills.badgeMine} + + )} + {isFromOthers && ( + + {t.settings.skills.badgeFromOthers} + + )} + {isOwned && skill.published && ( + + + {t.settings.skills.badgePublished} + + )} + {isCustom && !isBuiltin && skill.source === "agent" && ( + + {t.settings.skills.badgeSourceAgent} + + )} + {isCustom && !isBuiltin && skill.source === "upload" && ( + + {t.settings.skills.badgeSourceUpload} + + )} + {skill.featured && ( + + {t.settings.skills.badgeFeatured} + + )} + {skill.pinned && ( + + {t.settings.skills.badgePinned} + + )} +
+ ); + + const metaNodes = ( + <> + {skill.use_count !== undefined && ( +
+ {skill.use_count > 0 + ? t.settings.skills.useCountInline.replace( + "{count}", + String(skill.use_count), + ) + : t.settings.skills.useCountInlineZero} +
+ )} + {(skill.tags?.length ?? 0) > 0 && ( + + )} + + ); + + const actionButtons = ( + <> + {isAdmin && ( + + )} + {canManage && ( + + )} + {canManage && ( + + )} + {canTogglePublish && ( + + )} + + {canManage && ( + + )} + + ); + return ( <> - - setDetailOpen(true)} - > - -
- {desensitize(skill.name)} - {isBuiltin && ( - - {t.settings.skills.badgeBuiltin} - - )} - {isOwned && ( - - {t.settings.skills.badgeMine} - - )} - {isFromOthers && ( - - {t.settings.skills.badgeFromOthers} - - )} - {isOwned && skill.published && ( - - - {t.settings.skills.badgePublished} - - )} - {isCustom && !isBuiltin && skill.source === "agent" && ( - - {t.settings.skills.badgeSourceAgent} - - )} - {isCustom && !isBuiltin && skill.source === "upload" && ( - - {t.settings.skills.badgeSourceUpload} - - )} - {skill.featured && ( - - {t.settings.skills.badgeFeatured} - - )} - {skill.pinned && ( - - {t.settings.skills.badgePinned} - - )} -
-
- - {desensitize(skill.description ?? "")} - - {skill.use_count !== undefined && ( -
- {skill.use_count > 0 - ? t.settings.skills.useCountInline.replace( - "{count}", - String(skill.use_count), - ) - : t.settings.skills.useCountInlineZero} -
- )} - {(skill.tags?.length ?? 0) > 0 && ( - - )} -
- - {isAdmin && ( - - )} - {canManage && ( - - )} - {canManage && ( - - )} - {canTogglePublish && ( - - )} - - {canManage && ( - - )} - -
+ {badgeGroup} +

+ {desensitize(skill.description ?? "")} +

+ {metaNodes} +
+
+ {actionButtons} +
+
+ ) : ( + + setDetailOpen(true)} + > + {badgeGroup} + + {desensitize(skill.description ?? "")} + + {metaNodes} + + {actionButtons} + + )} diff --git a/frontend-web/src/components/workspace/view-mode-toggle.tsx b/frontend-web/src/components/workspace/view-mode-toggle.tsx new file mode 100644 index 00000000..552a61f5 --- /dev/null +++ b/frontend-web/src/components/workspace/view-mode-toggle.tsx @@ -0,0 +1,56 @@ +"use client"; + +import { LayoutGridIcon, ListIcon } from "lucide-react"; + +import { Button } from "@/components/ui/button"; +import type { ViewMode } from "@/core/system-settings/api"; +import { cn } from "@/lib/utils"; + +/** + * Compact card/list switch shown in the header of the skills and agents lists. + * Purely presentational — the parent owns the `value` (resolved from the user's + * local override or the admin default via `useViewMode`) and persists changes. + */ +export function ViewModeToggle({ + value, + onChange, + className, +}: { + value: ViewMode; + onChange: (mode: ViewMode) => void; + className?: string; +}) { + return ( +
+ + +
+ ); +} diff --git a/frontend-web/src/core/settings/local.ts b/frontend-web/src/core/settings/local.ts index f5db4fa1..cf6de232 100644 --- a/frontend-web/src/core/settings/local.ts +++ b/frontend-web/src/core/settings/local.ts @@ -55,6 +55,13 @@ export interface LocalSettings { * app sizes text/spacing in rem, the whole UI scales proportionally. */ fontScale: number; + /** + * Per-user override for the skill / agent list display mode (card vs list). + * `undefined` = follow the admin-configured system default. Set when the + * user flips the card/list toggle on the skills or agents page. + */ + skillViewMode?: "card" | "list"; + agentViewMode?: "card" | "list"; }; context: Omit< AgentThreadContext, diff --git a/frontend-web/src/core/system-settings/api.ts b/frontend-web/src/core/system-settings/api.ts index d94e38a4..9b5f3271 100644 --- a/frontend-web/src/core/system-settings/api.ts +++ b/frontend-web/src/core/system-settings/api.ts @@ -90,13 +90,18 @@ export async function loadPrivateSquareStatus(): Promise { } export type ThemeMode = "light" | "dark"; +export type ViewMode = "card" | "list"; export interface AppearanceSettings { default_theme: ThemeMode; + default_skill_view: ViewMode; + default_agent_view: ViewMode; } export interface AppearanceUpdate { default_theme?: ThemeMode; + default_skill_view?: ViewMode; + default_agent_view?: ViewMode; } export async function loadAppearanceSettings(): Promise { diff --git a/frontend-web/src/core/system-settings/hooks.ts b/frontend-web/src/core/system-settings/hooks.ts index 6cf4f5af..e5a85608 100644 --- a/frontend-web/src/core/system-settings/hooks.ts +++ b/frontend-web/src/core/system-settings/hooks.ts @@ -1,5 +1,7 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { useLocalSettings } from "@/core/settings"; + import { loadAppearanceSettings, loadCitationDisplaySettings, @@ -16,6 +18,7 @@ import { type LeaderboardSettingsUpdate, type PrivateSquareUpdate, type PromptPrefixUpdate, + type ViewMode, } from "./api"; const PROMPT_PREFIX_KEY = ["systemSettings", "promptPrefix"] as const; @@ -86,6 +89,31 @@ export function useUpdateAppearanceSettings() { }); } +/** + * Effective card/list display mode for the skill or agent list. A user's own + * choice (persisted in local settings) wins; otherwise the admin-configured + * system default applies; otherwise a built-in fallback (skills → list, + * agents → card, matching the historical layout of each page). `setMode` + * persists the user's override locally. + */ +export function useViewMode( + kind: "skill" | "agent", +): [ViewMode, (mode: ViewMode) => void] { + const [settings, setSettings] = useLocalSettings(); + const { data: appearance } = useAppearanceSettings(); + const overrideKey = kind === "skill" ? "skillViewMode" : "agentViewMode"; + const userOverride = settings.display[overrideKey]; + const adminDefault = + kind === "skill" + ? appearance?.default_skill_view + : appearance?.default_agent_view; + const fallback: ViewMode = kind === "skill" ? "list" : "card"; + const mode: ViewMode = userOverride ?? adminDefault ?? fallback; + const setMode = (next: ViewMode) => + setSettings("display", { [overrideKey]: next }); + return [mode, setMode]; +} + export function useCitationDisplaySettings() { return useQuery({ queryKey: CITATION_DISPLAY_KEY, diff --git a/frontend-web/src/pages/ScheduledTasksPage.tsx b/frontend-web/src/pages/ScheduledTasksPage.tsx index e5d3e614..418f807e 100644 --- a/frontend-web/src/pages/ScheduledTasksPage.tsx +++ b/frontend-web/src/pages/ScheduledTasksPage.tsx @@ -751,7 +751,24 @@ export default function ScheduledTasksPage() {
- 执行记录 +
+ 执行记录 + {selectedTask && ( + + )} +