Revert skill list layout and tune leaderboard report
- Revert skill settings to vertical list layout with pinned sorting - Default leaderboard range 30d -> 7d, hide stats while report generating - Reframe leaderboard report prompt as user-behavior analysis (cache v5) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
0d91c74289
commit
b117bb1dc2
3 files changed
+81
-294
No files matched your search
@@ -12,11 +12,10 @@ import {
|
|||||||
FileArchive,
|
FileArchive,
|
||||||
GlobeIcon,
|
GlobeIcon,
|
||||||
InfoIcon,
|
InfoIcon,
|
||||||
Loader2,
|
Loader2,
|
||||||
LockIcon,
|
LockIcon,
|
||||||
PinIcon,
|
PinIcon,
|
||||||
PinOffIcon,
|
RefreshCwIcon,
|
||||||
RefreshCwIcon,
|
|
||||||
SparklesIcon,
|
SparklesIcon,
|
||||||
TagsIcon,
|
TagsIcon,
|
||||||
Trash2Icon,
|
Trash2Icon,
|
||||||
@@ -27,15 +26,8 @@ import { useRouter } from "next/navigation";
|
|||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
|
||||||
Card,
|
|
||||||
CardDescription,
|
|
||||||
CardFooter,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
} from "@/components/ui/card";
|
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@@ -203,11 +195,22 @@ function SkillSettingsList({ onClose }: { onClose?: () => void }) {
|
|||||||
};
|
};
|
||||||
}, [skills, currentUserId]);
|
}, [skills, currentUserId]);
|
||||||
|
|
||||||
const filteredSkills = filter === "mine" ? activeMine : activeAll;
|
const filteredSkills = filter === "mine" ? activeMine : activeAll;
|
||||||
const staleCount = staleVisible.length;
|
const orderedSkills = useMemo(
|
||||||
const archivedCount = archivedVisible.length;
|
() =>
|
||||||
|
filteredSkills
|
||||||
const totalPages = Math.max(1, Math.ceil(filteredSkills.length / PAGE_SIZE));
|
.map((skill, index) => ({ skill, index }))
|
||||||
|
.sort((a, b) => {
|
||||||
|
const pinnedDelta = Number(!!b.skill.pinned) - Number(!!a.skill.pinned);
|
||||||
|
return pinnedDelta || a.index - b.index;
|
||||||
|
})
|
||||||
|
.map(({ skill }) => skill),
|
||||||
|
[filteredSkills],
|
||||||
|
);
|
||||||
|
const staleCount = staleVisible.length;
|
||||||
|
const archivedCount = archivedVisible.length;
|
||||||
|
|
||||||
|
const totalPages = Math.max(1, Math.ceil(orderedSkills.length / PAGE_SIZE));
|
||||||
// Filter / search / tag changes may shrink the result set — pull the page
|
// Filter / search / tag changes may shrink the result set — pull the page
|
||||||
// back into range without forcing a reset on every keystroke.
|
// back into range without forcing a reset on every keystroke.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -216,10 +219,10 @@ function SkillSettingsList({ onClose }: { onClose?: () => void }) {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setPage(1);
|
setPage(1);
|
||||||
}, [search, tagIds, sort]);
|
}, [search, tagIds, sort]);
|
||||||
const pagedSkills = useMemo(
|
const pagedSkills = useMemo(
|
||||||
() => filteredSkills.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE),
|
() => orderedSkills.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE),
|
||||||
[filteredSkills, page],
|
[orderedSkills, page],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleRescan = async () => {
|
const handleRescan = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -356,32 +359,18 @@ function SkillSettingsList({ onClose }: { onClose?: () => void }) {
|
|||||||
{!isLoading && !error && filteredSkills.length === 0 && (
|
{!isLoading && !error && filteredSkills.length === 0 && (
|
||||||
<EmptySkill />
|
<EmptySkill />
|
||||||
)}
|
)}
|
||||||
|
{!isLoading && !error && filteredSkills.length > 0 && (
|
||||||
{/* —— 原纵向列表布局(保留备查)——
|
<div className="flex flex-col gap-3">
|
||||||
{!isLoading && !error &&
|
{pagedSkills.map((skill) => (
|
||||||
filteredSkills.length > 0 &&
|
<SkillRow
|
||||||
pagedSkills.map((skill) => (
|
key={skill.name}
|
||||||
<SkillRow
|
skill={skill}
|
||||||
key={skill.name}
|
currentUserId={currentUserId}
|
||||||
skill={skill}
|
isAdmin={isAdmin}
|
||||||
currentUserId={currentUserId}
|
/>
|
||||||
isAdmin={isAdmin}
|
))}
|
||||||
/>
|
</div>
|
||||||
))} */}
|
)}
|
||||||
{/* card */}
|
|
||||||
{!isLoading && !error && filteredSkills.length > 0 && (
|
|
||||||
<div className="grid grid-cols-1 items-stretch gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
|
||||||
{pagedSkills.map((skill) => (
|
|
||||||
<SkillRow
|
|
||||||
key={skill.name}
|
|
||||||
skill={skill}
|
|
||||||
currentUserId={currentUserId}
|
|
||||||
isAdmin={isAdmin}
|
|
||||||
className="h-full"
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{!isLoading && !error && totalPages > 1 && (
|
{!isLoading && !error && totalPages > 1 && (
|
||||||
<div className="flex items-center justify-end gap-2 pt-1">
|
<div className="flex items-center justify-end gap-2 pt-1">
|
||||||
<Button
|
<Button
|
||||||
@@ -430,17 +419,15 @@ function SkillSettingsList({ onClose }: { onClose?: () => void }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SkillRow({
|
export function SkillRow({
|
||||||
skill,
|
skill,
|
||||||
currentUserId,
|
currentUserId,
|
||||||
isAdmin,
|
isAdmin,
|
||||||
className,
|
}: {
|
||||||
}: {
|
skill: Skill;
|
||||||
skill: Skill;
|
currentUserId: string | null;
|
||||||
currentUserId: string | null;
|
isAdmin: boolean;
|
||||||
isAdmin: boolean;
|
}) {
|
||||||
className?: string;
|
|
||||||
}) {
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const setPublished = useSetSkillPublished();
|
const setPublished = useSetSkillPublished();
|
||||||
const setPinned = useSetSkillPinned();
|
const setPinned = useSetSkillPinned();
|
||||||
@@ -525,9 +512,7 @@ export function SkillRow({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<Item className="w-full dark:border-[#213D5E] dark:bg-[#02244C] dark:text-white" variant="outline">
|
||||||
{/* —— 原 Item 列表行布局(保留备查)——
|
|
||||||
<Item className="w-full dark:border-[#213D5E] dark:bg-[#02244C] dark:text-white" variant="outline">
|
|
||||||
<ItemContent
|
<ItemContent
|
||||||
className="cursor-pointer"
|
className="cursor-pointer"
|
||||||
onClick={() => setDetailOpen(true)}
|
onClick={() => setDetailOpen(true)}
|
||||||
@@ -633,25 +618,24 @@ export function SkillRow({
|
|||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{canManage && (
|
{canManage && (
|
||||||
<Button
|
<Button
|
||||||
size="icon"
|
size="icon"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
className="h-8 w-8 shrink-0"
|
className={cn(
|
||||||
disabled={setPinned.isPending}
|
"h-8 w-8 shrink-0",
|
||||||
onClick={() => void handleTogglePinned()}
|
skill.pinned && "text-emerald-600 hover:text-emerald-700",
|
||||||
title={
|
)}
|
||||||
|
disabled={setPinned.isPending}
|
||||||
|
onClick={() => void handleTogglePinned()}
|
||||||
|
title={
|
||||||
skill.pinned
|
skill.pinned
|
||||||
? t.settings.skills.unpinAction
|
? t.settings.skills.unpinAction
|
||||||
: t.settings.skills.pinAction
|
: t.settings.skills.pinAction
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{skill.pinned ? (
|
<PinIcon className={cn("size-3.5", skill.pinned && "fill-current")} />
|
||||||
<PinOffIcon className="size-3.5" />
|
</Button>
|
||||||
) : (
|
)}
|
||||||
<PinIcon className="size-3.5" />
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
{canTogglePublish && (
|
{canTogglePublish && (
|
||||||
<Button
|
<Button
|
||||||
size="icon"
|
size="icon"
|
||||||
@@ -694,206 +678,8 @@ export function SkillRow({
|
|||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</ItemActions>
|
</ItemActions>
|
||||||
</Item> */}
|
</Item>
|
||||||
|
<Dialog open={duplicateOpen} onOpenChange={setDuplicateOpen}>
|
||||||
<Card
|
|
||||||
className={cn(
|
|
||||||
"group flex h-full min-w-0 cursor-pointer flex-col transition-shadow hover:shadow-md dark:!border-[#213D5E] dark:bg-[#02244C] dark:text-white",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
onClick={() => setDetailOpen(true)}
|
|
||||||
>
|
|
||||||
<CardHeader className="min-w-0 flex flex-col gap-0">
|
|
||||||
<div className="flex min-w-0 items-center gap-2">
|
|
||||||
<div className="bg-primary/10 text-primary flex h-9 w-9 shrink-0 items-center justify-center rounded-lg">
|
|
||||||
<SparklesIcon className="h-5 w-5" />
|
|
||||||
</div>
|
|
||||||
<div className="flex min-w-0 flex-1 flex-wrap items-center gap-2 overflow-hidden">
|
|
||||||
<CardTitle
|
|
||||||
className="min-w-0 flex-1 overflow-hidden text-base text-ellipsis whitespace-nowrap"
|
|
||||||
title={desensitize(skill.name)}
|
|
||||||
>
|
|
||||||
{desensitize(skill.name)}
|
|
||||||
</CardTitle>
|
|
||||||
<Badge variant="secondary" className="shrink-0 text-xs">
|
|
||||||
{isBuiltin
|
|
||||||
? t.settings.skills.badgeBuiltin
|
|
||||||
: skill.category}
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-4 mb-1.5 flex min-h-5 flex-wrap items-center gap-1">
|
|
||||||
{isOwned && (
|
|
||||||
<Badge
|
|
||||||
variant="default"
|
|
||||||
className="bg-emerald-500/15 text-emerald-700 hover:bg-emerald-500/15 dark:text-emerald-300 text-xs"
|
|
||||||
>
|
|
||||||
{t.settings.skills.badgeMine}
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
{isFromOthers && (
|
|
||||||
<Badge
|
|
||||||
variant="outline"
|
|
||||||
className="border-blue-500/30 text-blue-700 dark:text-blue-300 text-xs"
|
|
||||||
title={
|
|
||||||
skill.owner_user_id
|
|
||||||
? t.settings.skills.badgeFromOthersTitle.replace(
|
|
||||||
"{user_id}",
|
|
||||||
skill.owner_user_id,
|
|
||||||
)
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{t.settings.skills.badgeFromOthers}
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
{isOwned && skill.published && (
|
|
||||||
<Badge
|
|
||||||
variant="outline"
|
|
||||||
className="border-sky-500/30 text-sky-700 dark:text-sky-300 text-xs"
|
|
||||||
title={t.settings.skills.badgePublishedTitle}
|
|
||||||
>
|
|
||||||
<GlobeIcon className="size-3" />
|
|
||||||
{t.settings.skills.badgePublished}
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
{isCustom && !isBuiltin && skill.source === "agent" && (
|
|
||||||
<Badge
|
|
||||||
variant="outline"
|
|
||||||
className="border-cyan-500/30 text-cyan-700 dark:text-cyan-300 text-xs"
|
|
||||||
title={t.settings.skills.badgeSourceAgentTitle}
|
|
||||||
>
|
|
||||||
{t.settings.skills.badgeSourceAgent}
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
{isCustom && !isBuiltin && skill.source === "upload" && (
|
|
||||||
<Badge
|
|
||||||
variant="outline"
|
|
||||||
className="border-zinc-400/40 text-zinc-600 dark:text-zinc-400 text-xs"
|
|
||||||
title={t.settings.skills.badgeSourceUploadTitle}
|
|
||||||
>
|
|
||||||
{t.settings.skills.badgeSourceUpload}
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
{skill.pinned && (
|
|
||||||
<Badge
|
|
||||||
variant="outline"
|
|
||||||
className="border-purple-500/30 text-purple-700 dark:text-purple-300 text-xs"
|
|
||||||
title={t.settings.skills.badgePinnedTitle}
|
|
||||||
>
|
|
||||||
{t.settings.skills.badgePinned}
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="mt-1.5 min-h-10 mb-3">
|
|
||||||
{skill.description ? (
|
|
||||||
<CardDescription className="line-clamp-2 text-sm">
|
|
||||||
{desensitize(skill.description)}
|
|
||||||
</CardDescription>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
<div className="text-muted-foreground mt-0.5 min-h-4 text-xs leading-tight">
|
|
||||||
{skill.use_count !== undefined && skill.use_count > 0
|
|
||||||
? t.settings.skills.useCountInline.replace(
|
|
||||||
"{count}",
|
|
||||||
String(skill.use_count),
|
|
||||||
)
|
|
||||||
: t.settings.skills.useCountInlineZero}
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"min-w-0 max-w-full overflow-hidden pb-0",
|
|
||||||
(skill.tags?.length ?? 0) > 0 ? "pt-2.5" : "min-h-4",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{(skill.tags?.length ?? 0) > 0 && (
|
|
||||||
<TagChipList tags={skill.tags!} className="w-full" />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</CardHeader>
|
|
||||||
|
|
||||||
<CardFooter
|
|
||||||
className="cursor-default py-0 pt-1.5"
|
|
||||||
onClick={(e) => e.stopPropagation()}
|
|
||||||
>
|
|
||||||
<div className="flex w-full flex-wrap items-center justify-end gap-1">
|
|
||||||
{isAdmin && (
|
|
||||||
<Button
|
|
||||||
size="icon"
|
|
||||||
variant="ghost"
|
|
||||||
className="h-8 w-8 shrink-0"
|
|
||||||
onClick={() => setTagPickerOpen(true)}
|
|
||||||
title={t.adminTags.manageTags}
|
|
||||||
>
|
|
||||||
<TagsIcon className="h-3.5 w-3.5" />
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
{canManage && (
|
|
||||||
<Button
|
|
||||||
size="icon"
|
|
||||||
variant="ghost"
|
|
||||||
className="h-8 w-8 shrink-0"
|
|
||||||
disabled={setPinned.isPending}
|
|
||||||
onClick={() => void handleTogglePinned()}
|
|
||||||
title={
|
|
||||||
skill.pinned
|
|
||||||
? t.settings.skills.unpinAction
|
|
||||||
: t.settings.skills.pinAction
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{skill.pinned ? (
|
|
||||||
<PinOffIcon className="h-3.5 w-3.5" />
|
|
||||||
) : (
|
|
||||||
<PinIcon className="h-3.5 w-3.5" />
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
{canTogglePublish && (
|
|
||||||
<Button
|
|
||||||
size="icon"
|
|
||||||
variant="ghost"
|
|
||||||
className="h-8 w-8 shrink-0"
|
|
||||||
disabled={setPublished.isPending}
|
|
||||||
onClick={() => void handleTogglePublished(!skill.published)}
|
|
||||||
title={
|
|
||||||
skill.published
|
|
||||||
? t.settings.skills.unpublishAction
|
|
||||||
: t.settings.skills.publishAction
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{skill.published ? (
|
|
||||||
<LockIcon className="h-3.5 w-3.5" />
|
|
||||||
) : (
|
|
||||||
<GlobeIcon className="h-3.5 w-3.5" />
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
<Button
|
|
||||||
size="icon"
|
|
||||||
variant="ghost"
|
|
||||||
className="h-8 w-8 shrink-0"
|
|
||||||
disabled={duplicateSkillMutation.isPending}
|
|
||||||
onClick={handleOpenDuplicate}
|
|
||||||
title="复制为新技能"
|
|
||||||
>
|
|
||||||
<CopyIcon className="h-3.5 w-3.5" />
|
|
||||||
</Button>
|
|
||||||
{canManage && (
|
|
||||||
<Button
|
|
||||||
size="icon"
|
|
||||||
variant="ghost"
|
|
||||||
className="text-destructive hover:text-destructive h-8 w-8 shrink-0"
|
|
||||||
onClick={() => setConfirmDeleteOpen(true)}
|
|
||||||
title={t.settings.skills.deleteAction}
|
|
||||||
>
|
|
||||||
<Trash2Icon className="h-3.5 w-3.5" />
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</CardFooter>
|
|
||||||
</Card>
|
|
||||||
<Dialog open={duplicateOpen} onOpenChange={setDuplicateOpen}>
|
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>复制技能</DialogTitle>
|
<DialogTitle>复制技能</DialogTitle>
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ function toDateInput(date: Date): string {
|
|||||||
return `${year}-${month}-${day}`;
|
return `${year}-${month}-${day}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function defaultRange(days = 30): RangeState {
|
function defaultRange(days = 7): RangeState {
|
||||||
const end = new Date();
|
const end = new Date();
|
||||||
const start = new Date();
|
const start = new Date();
|
||||||
start.setDate(end.getDate() - days + 1);
|
start.setDate(end.getDate() - days + 1);
|
||||||
@@ -108,7 +108,7 @@ function defaultRange(days = 30): RangeState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function rangeForApi(range: RangeState) {
|
function rangeForApi(range: RangeState) {
|
||||||
const normalized = range.since && range.until ? range : defaultRange(30);
|
const normalized = range.since && range.until ? range : defaultRange(7);
|
||||||
const start = new Date(`${normalized.since}T00:00:00`);
|
const start = new Date(`${normalized.since}T00:00:00`);
|
||||||
const end = new Date(`${normalized.until}T23:59:59`);
|
const end = new Date(`${normalized.until}T23:59:59`);
|
||||||
return {
|
return {
|
||||||
@@ -1035,7 +1035,7 @@ function UserDetailPanel({
|
|||||||
|
|
||||||
export default function AdminUserLeaderboardPage() {
|
export default function AdminUserLeaderboardPage() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [range, setRange] = useState<RangeState>(() => defaultRange(30));
|
const [range, setRange] = useState<RangeState>(() => defaultRange(7));
|
||||||
const [tab, setTab] = useState<UserRankTab>("activity");
|
const [tab, setTab] = useState<UserRankTab>("activity");
|
||||||
const [query, setQuery] = useState("");
|
const [query, setQuery] = useState("");
|
||||||
const [groupRules, setGroupRules] = useState("zero,two");
|
const [groupRules, setGroupRules] = useState("zero,two");
|
||||||
@@ -1288,7 +1288,7 @@ export default function AdminUserLeaderboardPage() {
|
|||||||
{reportGenerating ? "生成中" : "生成报告"}
|
{reportGenerating ? "生成中" : "生成报告"}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{data && (
|
{data && reportStatus === "idle" && !reportText && (
|
||||||
<div className="mx-6 mt-4 grid shrink-0 gap-3 md:grid-cols-3">
|
<div className="mx-6 mt-4 grid shrink-0 gap-3 md:grid-cols-3">
|
||||||
<section className="rounded-lg border border-[#F1D6D6] bg-white p-3 dark:border-[#462123] dark:bg-[#230D0F]">
|
<section className="rounded-lg border border-[#F1D6D6] bg-white p-3 dark:border-[#462123] dark:bg-[#230D0F]">
|
||||||
<div className="mb-2 flex items-center gap-2 text-sm font-semibold text-[#231815] dark:text-white">
|
<div className="mb-2 flex items-center gap-2 text-sm font-semibold text-[#231815] dark:text-white">
|
||||||
|
|||||||
@@ -318,7 +318,7 @@ def _report_cache_key(
|
|||||||
settings,
|
settings,
|
||||||
) -> str:
|
) -> str:
|
||||||
payload = {
|
payload = {
|
||||||
"version": 4,
|
"version": 5,
|
||||||
"since": _iso(since),
|
"since": _iso(since),
|
||||||
"until": _iso(until),
|
"until": _iso(until),
|
||||||
"limit": limit,
|
"limit": limit,
|
||||||
@@ -1091,15 +1091,16 @@ async def _build_leaderboard_report_prompt(
|
|||||||
"trends": [t.model_dump() for t in trends[-21:]],
|
"trends": [t.model_dump() for t in trends[-21:]],
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
"请基于以下管理员排行榜统计数据,生成一份中文运营分析报告。"
|
"请基于以下管理员排行榜统计数据,生成一份中文用户行为分析报告。"
|
||||||
"报告需要有结论,不要只罗列数字;请使用 Markdown 标题、列表和表格,让前端可以直接渲染。"
|
"报告需要有结论,不要只罗列数字;请使用 Markdown 标题、列表和表格,让前端可以直接渲染。"
|
||||||
"重点要求:"
|
"重点要求:"
|
||||||
"1) 优先突出用户最近关注的实时问题、事件、主题和业务方向,必须引用 recent_questions 中的近期问题作为依据;"
|
"1) 你是专门负责分析用户行为的专家,不要自称企业 AI 应用运营分析师;"
|
||||||
"2) 单独分析 frequent_questions,归纳高频关注簇,说明这些高频问题反映出的需求强度;"
|
"2) 优先突出用户最近常用问题、最近关注的实时问题、事件、主题和业务方向,必须引用 recent_questions 中的近期问题作为依据;"
|
||||||
"3) 结合 top_users、top_skills、top_tools、top_models 和 trends,说明使用频率、使用增长、常用能力和重点用户画像;"
|
"3) 单独分析 frequent_questions,归纳高频关注簇,说明这些高频问题反映出的需求强度和重复频率;"
|
||||||
"4) 可以提及使用强度、关注变化和运营机会,但不要把报告重点放在系统本身的问题、故障、失败率或回答质量缺陷上;"
|
"4) 结合 top_users、top_skills、top_tools、top_models 和 trends,说明使用频率、使用增长、常用能力和重点用户行为画像;"
|
||||||
"5) 给管理员 3-5 条可执行建议,建议应围绕用户关注主题、资源配置、内容供给、培训引导和高频能力优化。"
|
"5) 可以提及使用强度、关注变化和行为机会,但不要把报告重点放在系统本身的问题、故障、失败率或回答质量缺陷上;"
|
||||||
"输出结构:摘要、最近关注、高频问题与使用频率、重点用户与常用能力、趋势观察、行动建议。"
|
"6) 给管理员 3-5 条可执行建议,建议应围绕用户关注主题、资源配置、内容供给、培训引导和高频能力优化。"
|
||||||
|
"输出结构:摘要、最近关注、高频问题与频率、重点用户行为、常用能力、趋势观察、行动建议。"
|
||||||
"\n\n统计数据如下:\n"
|
"\n\n统计数据如下:\n"
|
||||||
f"{json.dumps(context, ensure_ascii=False, default=str)}"
|
f"{json.dumps(context, ensure_ascii=False, default=str)}"
|
||||||
)
|
)
|
||||||
@@ -1141,7 +1142,7 @@ async def generate_user_leaderboard_report(
|
|||||||
model = create_chat_model(name=resolved_model_name, thinking_enabled=thinking_enabled, app_config=config)
|
model = create_chat_model(name=resolved_model_name, thinking_enabled=thinking_enabled, app_config=config)
|
||||||
response = await model.ainvoke(
|
response = await model.ainvoke(
|
||||||
[
|
[
|
||||||
SystemMessage(content="你是企业 AI 应用的运营分析师,擅长从用户行为、问题文本和调用指标中生成管理报告;报告聚焦用户关注主题和使用频率,不讨论系统本身问题。"),
|
SystemMessage(content="你是专门负责分析用户行为的专家,擅长从用户最近常用问题、关注主题、使用频率和调用指标中生成管理报告;报告聚焦用户行为,不讨论系统本身问题。"),
|
||||||
HumanMessage(content=prompt),
|
HumanMessage(content=prompt),
|
||||||
],
|
],
|
||||||
config={"run_name": "admin_user_leaderboard_report"},
|
config={"run_name": "admin_user_leaderboard_report"},
|
||||||
@@ -1206,7 +1207,7 @@ async def stream_user_leaderboard_report(
|
|||||||
model = create_chat_model(name=resolved_model_name, thinking_enabled=thinking_enabled, app_config=config)
|
model = create_chat_model(name=resolved_model_name, thinking_enabled=thinking_enabled, app_config=config)
|
||||||
async for chunk in model.astream(
|
async for chunk in model.astream(
|
||||||
[
|
[
|
||||||
SystemMessage(content="你是企业 AI 应用的运营分析师,擅长从用户行为、问题文本和调用指标中生成管理报告;报告聚焦用户关注主题和使用频率,不讨论系统本身问题。"),
|
SystemMessage(content="你是专门负责分析用户行为的专家,擅长从用户最近常用问题、关注主题、使用频率和调用指标中生成管理报告;报告聚焦用户行为,不讨论系统本身问题。"),
|
||||||
HumanMessage(content=prompt),
|
HumanMessage(content=prompt),
|
||||||
],
|
],
|
||||||
config={"run_name": "admin_user_leaderboard_report"},
|
config={"run_name": "admin_user_leaderboard_report"},
|
||||||
|
|||||||
Reference in new issue
Block a user