diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 45dc51ee..0ff43fa7 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -7,6 +7,7 @@ import { type WheelEvent, } from "react"; import { + ArrowLeft, Check, ChevronDown, CircleAlert, @@ -21,6 +22,7 @@ import { motion } from "motion/react"; import { cancelAgentkitDeployment, addSessionCapability, + clearMessageFeedbackCache, createSession, DEFAULT_STUDIO_ACCESS, DEFAULT_SITE_BRANDING, @@ -45,6 +47,7 @@ import { type AgentInfo, type AgentNode, type AgentTarget, + type AgentFeedbackCase, type AdkSession, type AddSessionCapability, type Attachment, @@ -944,6 +947,14 @@ export default function App() { const [searchView, setSearchView] = useState(false); // The #748 Agent workspace: library, evaluation groups, and draft management. const [manageAgents, setManageAgents] = useState(false); + const [feedbackCaseReturnAgentId, setFeedbackCaseReturnAgentId] = useState(""); + const [feedbackCaseReturnKind, setFeedbackCaseReturnKind] = + useState<"good" | "bad">("good"); + const [focusedWorkspaceAgentSection, setFocusedWorkspaceAgentSection] = + useState<"basic" | "evaluations">("basic"); + const [focusedWorkspaceCaseKind, setFocusedWorkspaceCaseKind] = + useState<"good" | "bad">("good"); + const [feedbackTargetEventId, setFeedbackTargetEventId] = useState(""); // A search result may belong to a different agent; remember it so the // agent-switch effect opens it instead of resetting to a fresh chat. const pendingOpenRef = useRef<{ app: string; sid: string } | null>(null); @@ -1213,6 +1224,7 @@ export default function App() { setRuntimeUpdateTarget(null); setFocusedDeploymentTaskId(""); setFocusedWorkspaceAgentId(agentId); + setFocusedWorkspaceAgentSection("basic"); setCreateView(null); setManageAgents(true); setAppName(agentId); @@ -1224,6 +1236,7 @@ export default function App() { setAddMenu(false); setManageAgents(true); setFocusedWorkspaceAgentId(""); + setFocusedWorkspaceAgentSection("basic"); setFocusedDeploymentTaskId(task.id); setError(""); }, []); @@ -1250,6 +1263,7 @@ export default function App() { editingDraftBaselineRef.current = null; setFocusedDeploymentTaskId(""); setFocusedWorkspaceAgentId(agentId); + setFocusedWorkspaceAgentSection("basic"); setCreateView(null); setManageAgents(true); setAppName(agentId); @@ -1257,6 +1271,7 @@ export default function App() { [editingDraftId, removeWorkspaceDraft, runtimeUpdateTarget], ); const scrollRef = useRef(null); + const turnNodeRefs = useRef>(new Map()); const conversationAutoFollowRef = useRef(true); const conversationSmoothScrollRef = useRef(false); const conversationSmoothTimerRef = useRef(null); @@ -1302,6 +1317,17 @@ export default function App() { ) return; el.scrollTop = el.scrollHeight; }, [activeConversationBusy, turns]); + useEffect(() => { + if (!feedbackTargetEventId || manageAgents || turns.length === 0) return; + const node = turnNodeRefs.current.get(feedbackTargetEventId); + if (!node) return; + conversationAutoFollowRef.current = false; + node.scrollIntoView({ behavior: "smooth", block: "center" }); + const timer = window.setTimeout(() => { + setFeedbackTargetEventId(""); + }, 2600); + return () => window.clearTimeout(timer); + }, [feedbackTargetEventId, manageAgents, turns]); useEffect(() => () => { if (conversationSmoothTimerRef.current !== null) { window.clearTimeout(conversationSmoothTimerRef.current); @@ -1490,6 +1516,13 @@ export default function App() { if (!access.capabilities.manageAgents) setManageAgents(false); }, [access]); + useEffect(() => { + if (authStatus !== "authenticated" || agentsSource !== "cloud" || !uiConfigLoaded) { + return; + } + void refreshAgentLibrary(); + }, [agentsSource, authStatus, refreshAgentLibrary, uiConfigLoaded]); + useEffect(() => { document.title = siteBranding.title; let favicon = document.querySelector('link[rel~="icon"]'); @@ -1556,10 +1589,16 @@ export default function App() { // Restore the last-used agent; otherwise land on a known-good default // (prefer a servable, conversational agent — numbered examples like // 01_quickstart are standalone scripts with no root_agent and can't load). - // Cloud mode: nothing is selected by default — the user picks a runtime - // each session (the sidebar shows the red "请选择 Agent" prompt until then). if (agentsSource === "cloud") { - setAppName(""); + const saved = localStorage.getItem(LS.app); + const remoteIds = connections.flatMap((c) => + c.apps.map((a) => remoteAppId(c.id, a)), + ); + setAppName((current) => { + if (current && remoteIds.includes(current)) return current; + if (saved && remoteIds.includes(saved)) return saved; + return remoteIds[0] ?? ""; + }); return; } // Local mode: restore the last-used agent, else a known-good default @@ -1575,7 +1614,7 @@ export default function App() { setAppName(valid ? saved : fallback || ""); }) .catch((e) => setError(String(e))); - }, [authStatus, agentsSource]); + }, [authStatus, agentsSource, connections]); // Persist the current view/agent/session so a refresh restores them. useEffect(() => { @@ -1957,6 +1996,112 @@ export default function App() { } } + async function openFeedbackCaseInStudio(item: AgentFeedbackCase) { + if (!item.sessionId || !item.messageId) { + setError("这条案例缺少会话定位信息,无法跳转。"); + return; + } + setSearchView(false); + setCreateView(null); + setAddAgent(false); + setAddMenu(false); + setSkillCenter(false); + setManageAgents(false); + setFeedbackCaseReturnAgentId(appName); + setFeedbackCaseReturnKind(item.kind); + setFeedbackTargetEventId(item.messageId); + await pickSession(item.sessionId); + } + + function returnToFeedbackCases() { + const agentId = feedbackCaseReturnAgentId || appName; + setSearchView(false); + setCreateView(null); + setAddAgent(false); + setAddMenu(false); + setSkillCenter(false); + setFocusedDeploymentTaskId(""); + setFocusedWorkspaceAgentId(agentId); + setFocusedWorkspaceAgentSection("evaluations"); + setFocusedWorkspaceCaseKind(feedbackCaseReturnKind); + setManageAgents(true); + setFeedbackCaseReturnAgentId(""); + setFeedbackTargetEventId(""); + } + + function clearDeletedFeedbackCases(items: AgentFeedbackCase[]) { + const clearBySession = new Map>(); + const clearByCacheScope = new Map; + }>(); + for (const item of items) { + if (!item.sessionId || !item.messageId) continue; + const sessionEvents = clearBySession.get(item.sessionId) ?? new Set(); + sessionEvents.add(item.messageId); + clearBySession.set(item.sessionId, sessionEvents); + if (item.runtimeId && item.userId) { + const key = [ + item.runtimeId, + appName, + item.userId, + item.sessionId, + ].join(":"); + const scope = clearByCacheScope.get(key) ?? { + runtimeId: item.runtimeId, + appName, + userId: item.userId, + sessionId: item.sessionId, + eventIds: new Set(), + }; + scope.eventIds.add(item.messageId); + clearByCacheScope.set(key, scope); + } + } + if (clearBySession.size === 0) return; + setTurnsBySession((current) => { + const next = { ...current }; + for (const [sid, eventIds] of clearBySession) { + const existing = next[sid]; + if (!existing) continue; + next[sid] = existing.map((turn) => + turn.meta?.eventId && eventIds.has(turn.meta.eventId) + ? { ...turn, meta: { ...turn.meta, feedback: undefined } } + : turn, + ); + } + return next; + }); + setSessions((current) => + current.map((session) => { + const eventIds = clearBySession.get(session.id); + if (!eventIds || !session.state) return session; + const state = { ...session.state }; + for (const eventId of eventIds) delete state[`veadk_feedback:${eventId}`]; + return { ...session, state }; + }), + ); + setFeedbackPendingIds((current) => { + const next = new Set(current); + for (const eventIds of clearBySession.values()) { + for (const eventId of eventIds) next.delete(eventId); + } + return next; + }); + for (const scope of clearByCacheScope.values()) { + clearMessageFeedbackCache({ + runtimeId: scope.runtimeId, + appName: scope.appName, + userId: scope.userId, + sessionId: scope.sessionId, + eventIds: [...scope.eventIds], + }); + } + } + async function ensureSession(activate = true): Promise { if (sessionId) return sessionId; if (!creatingSessionRef.current) { @@ -2492,6 +2637,25 @@ export default function App() { setAppName(id); }; + const talkToWorkspaceAgent = (id: string) => { + setCreateView(null); + setSkillCenter(false); + setAddAgent(false); + setAddMenu(false); + setSearchView(false); + setManageAgents(false); + setFeedbackCaseReturnAgentId(""); + setFeedbackTargetEventId(""); + selectAgent(id); + }; + + const selectWorkspaceAgentFromNavbar = (id: string) => { + setFocusedDeploymentTaskId(""); + setFocusedWorkspaceAgentId(id); + setFocusedWorkspaceAgentSection("basic"); + selectAgent(id); + }; + return (
0 && @@ -2824,6 +2986,20 @@ export default function App() { 加载会话…
)} + {feedbackCaseReturnAgentId && + !showManageAgents && + !showAddMenu && + !showAddAgent && + !searchView && + !skillCenter && + visibleCreateView === null && ( +
+ +
+ )} {showManageAgents ? ( void refreshAgentLibrary()} onAgentOrderChange={saveWorkspaceAgentOrder} onDeleteAgents={deleteWorkspaceAgents} onDeleteDrafts={deleteWorkspaceDrafts} onSelectAgent={selectAgent} + onTalkAgent={talkToWorkspaceAgent} + onOpenFeedbackCase={(item) => void openFeedbackCaseInStudio(item)} + onFeedbackCasesDeleted={clearDeletedFeedbackCases} onCreateAgent={() => { if (!canCreateAgents) { setError("当前账号没有添加 Agent 的权限。"); @@ -3137,7 +3318,19 @@ export default function App() { return ( { + if (!feedbackEventId) return; + if (node) { + turnNodeRefs.current.set(feedbackEventId, node); + } else { + turnNodeRefs.current.delete(feedbackEventId); + } + }} + className={[ + "turn turn--assistant", + isSubAgent ? "turn--subagent" : "", + feedbackTargetEventId === feedbackEventId ? "is-feedback-target" : "", + ].filter(Boolean).join(" ")} initial={{ opacity: 0, y: 8 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.2, ease: "easeOut" }} diff --git a/frontend/src/adk/client.ts b/frontend/src/adk/client.ts index fd9adc69..ecfae5f6 100644 --- a/frontend/src/adk/client.ts +++ b/frontend/src/adk/client.ts @@ -93,6 +93,43 @@ export interface MessageFeedbackState { updatedAt: number; } +export interface AgentFeedbackSetSummary { + kind: MessageFeedbackRating; + evaluationSetId: string | null; + evaluationSetName: string | null; + workspaceId: string | null; + itemCount: number; +} + +export interface AgentFeedbackCase { + id: string; + itemKey: string; + kind: MessageFeedbackRating; + input: string; + output: string; + referenceOutput: string; + comment: string; + agentName: string; + sessionId: string; + messageId: string; + runtimeId: string; + invocationId: string; + userId: string; + createdAt: string; + evaluationSetId: string; + evaluationSetName: string; + workspaceId: string; +} + +export interface AgentFeedbackCasesResponse { + agentName: string; + runtimeId: string; + region: string; + projectName: string; + sets: AgentFeedbackSetSummary[]; + items: AgentFeedbackCase[]; +} + const MESSAGE_FEEDBACK_CACHE_KEY = "veadk.messageFeedback.v1"; function feedbackCacheScope( @@ -131,6 +168,34 @@ function storeMessageFeedback( localStorage.setItem(MESSAGE_FEEDBACK_CACHE_KEY, JSON.stringify(cache)); } +export function clearMessageFeedbackCache(args: { + runtimeId: string; + appName: string; + userId: string; + sessionId: string; + eventIds: string[]; +}): void { + if (typeof window === "undefined") return; + const scope = feedbackCacheScope( + args.runtimeId, + args.appName, + args.userId, + args.sessionId, + ); + const cache = readMessageFeedbackCache(); + const scoped = cache[scope]; + if (!scoped) return; + for (const eventId of args.eventIds) { + delete scoped[`veadk_feedback:${eventId}`]; + } + if (Object.keys(scoped).length === 0) { + delete cache[scope]; + } else { + cache[scope] = scoped; + } + localStorage.setItem(MESSAGE_FEEDBACK_CACHE_KEY, JSON.stringify(cache)); +} + export interface AdkInlineData { mimeType?: string; data?: string; // base64 (no data: prefix) @@ -483,6 +548,52 @@ export async function submitMessageFeedback(args: { return feedback; } +export async function getAgentFeedbackCases(args: { + runtimeId: string; + region?: string; + appName: string; + pageSize?: number; +}): Promise { + const query = new URLSearchParams({ + runtimeId: args.runtimeId, + region: args.region ?? "cn-beijing", + appName: args.appName, + page_size: String(args.pageSize ?? 100), + }); + const res = await apiFetch(`/web/evaluation/feedback-cases?${query.toString()}`); + if (!res.ok) { + throw new Error(await httpErrorMessage(res, "读取评测集失败")); + } + return res.json(); +} + +export async function deleteAgentFeedbackCases(args: { + runtimeId: string; + region?: string; + appName: string; + itemIds: string[]; +}): Promise<{ deletedCount: number }> { + const res = await apiFetch( + "/web/evaluation/feedback-cases/delete", + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + runtimeId: args.runtimeId, + region: args.region ?? "cn-beijing", + appName: args.appName, + itemIds: args.itemIds, + }), + }, + {}, + TRANSFER_REQUEST_TIMEOUT_MS, + ); + if (!res.ok) { + throw new Error(await httpErrorMessage(res, "删除评测案例失败")); + } + return res.json(); +} + export async function deleteSession( appName: string, userId: string, diff --git a/frontend/src/create/CustomCreate.css b/frontend/src/create/CustomCreate.css index 5068852c..2b710b51 100644 --- a/frontend/src/create/CustomCreate.css +++ b/frontend/src/create/CustomCreate.css @@ -1613,6 +1613,12 @@ color: hsl(var(--muted-foreground)); font-size: 10.5px; } +.cw-ab-ready-title { + color: hsl(var(--foreground)); + font-size: 20px; + font-weight: 760; + line-height: 1.1; +} .cw-ab-starting { align-content: center; gap: 8px; @@ -1655,9 +1661,15 @@ .cw-ab-deploy-footer { flex: 0 0 auto; display: flex; + align-items: center; justify-content: flex-end; + gap: 8px; padding: 0 12px 12px; } +.cw-ab-footer-start { + min-width: 0; + background: hsl(var(--secondary) / 0.58); +} .cw-ab-deploy { min-height: 32px; padding: 0 13px; diff --git a/frontend/src/create/CustomCreate.tsx b/frontend/src/create/CustomCreate.tsx index fccd725b..d4643e04 100644 --- a/frontend/src/create/CustomCreate.tsx +++ b/frontend/src/create/CustomCreate.tsx @@ -1909,9 +1909,6 @@ function DebugComparisonWorkspace({ { - await onStartVariant(variant.id); - }} /> ) : starting ? (
@@ -1921,39 +1918,21 @@ function DebugComparisonWorkspace({ ) : stale ? (
配置已变更,请重新启动此环境 -
) : variant.messages.length === 0 ? (
- - {disabledReason ? ( + {ready ? ( + <> + 已就绪 + + 可在下方输入测试消息 + + + ) : ( - {disabledReason} + {disabledReason || "启动环境后即可加入本轮测试"} - ) : ready ? ( - 等待测试输入 - ) : null} + )}
) : ( variant.messages.map((message, index) => ( @@ -1984,6 +1963,20 @@ function DebugComparisonWorkspace({