feat(workbench): redesign investigation reports and diagrams
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { toAgentChatMessages, toAgentUiMessages } from "./agent-session-message-state";
|
||||
|
||||
describe("task context in persisted answers", () => {
|
||||
it("keeps the report reference of each answer when history is restored", () => {
|
||||
const reference = {
|
||||
task_id: "22222222-2222-4222-8222-222222222222",
|
||||
execution_generation: 1,
|
||||
report_revision: 3
|
||||
};
|
||||
const stored = [
|
||||
{
|
||||
id: "answer-1",
|
||||
role: "assistant",
|
||||
parts: [
|
||||
{ type: "data-task_context", data: reference },
|
||||
{ type: "text", text: "该判断仅覆盖投诉时段的上游记录。" }
|
||||
]
|
||||
}
|
||||
];
|
||||
const messages = toAgentChatMessages(toAgentUiMessages(stored), {}, {});
|
||||
expect(messages[0]?.taskContext).toEqual(reference);
|
||||
expect(messages[0]?.content).toBe("该判断仅覆盖投诉时段的上游记录。");
|
||||
});
|
||||
it("keeps the answer readable when a report reference is invalid", () => {
|
||||
const messages = toAgentChatMessages(
|
||||
toAgentUiMessages([
|
||||
{
|
||||
id: "answer-1",
|
||||
role: "assistant",
|
||||
parts: [
|
||||
{
|
||||
type: "data-task_context",
|
||||
data: { task_id: "unknown", execution_generation: -1, report_revision: 0 }
|
||||
},
|
||||
{ type: "text", text: "报告引用未记录。" }
|
||||
]
|
||||
}
|
||||
]),
|
||||
{},
|
||||
{}
|
||||
);
|
||||
expect(messages[0]?.taskContext).toBeUndefined();
|
||||
expect(messages[0]?.content).toBe("报告引用未记录。");
|
||||
});
|
||||
});
|
||||
@@ -2,6 +2,7 @@ import type { UIMessage } from "ai";
|
||||
import type { Dispatch, SetStateAction } from "react";
|
||||
import {
|
||||
applyPermissionResponse,
|
||||
agentTaskContextReferenceSchema,
|
||||
applyQuestionResponse,
|
||||
toTodoUpdate,
|
||||
upsertPermission,
|
||||
@@ -16,6 +17,7 @@ import {
|
||||
} from "@/features/agent";
|
||||
|
||||
type AgentUiDataParts = {
|
||||
task_context: Record<string, unknown>;
|
||||
execution_plan: Record<string, unknown>;
|
||||
progress: Record<string, unknown>;
|
||||
todo_update: Record<string, unknown>;
|
||||
@@ -123,7 +125,10 @@ export function toAgentChatMessages(
|
||||
if (!isAgentDataPart(part)) {
|
||||
continue;
|
||||
}
|
||||
if (part.type === "data-progress") {
|
||||
if (part.type === "data-task_context") {
|
||||
const parsed = agentTaskContextReferenceSchema.safeParse(part.data);
|
||||
if (parsed.success) next = { ...next, taskContext: parsed.data };
|
||||
} else if (part.type === "data-progress") {
|
||||
next = {
|
||||
...next,
|
||||
progress: upsertProgress(next.progress, part.data)
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
executeOperationalTaskAction,
|
||||
fetchOperationalTask,
|
||||
fetchOperationalTaskEvidenceSnapshot,
|
||||
fetchOperationalTaskReports,
|
||||
fetchOperationalTaskNotifications,
|
||||
reconcileOperationalTaskAction,
|
||||
type OperationalTask,
|
||||
@@ -172,7 +173,10 @@ export function useOperationalTasks(
|
||||
};
|
||||
}, [acknowledgeEventsThrough, eventRetryGeneration, getAccessToken, syncEvents]);
|
||||
|
||||
const conditions = useMemo(() => tasks.map(toOperationalTaskCondition), [tasks]);
|
||||
const conditions = useMemo(() => tasks.map((task) => ({ ...toOperationalTaskCondition(task),
|
||||
hasNewReport: notifications.some((item) => item.task_id === task.task_id && item.kind === "report_updated"
|
||||
&& item.report_reference != null && item.lifecycle_status === "active" && item.status === "unread"),
|
||||
})), [tasks, notifications]);
|
||||
|
||||
const createInvestigation = useCallback(async (input: CreateInvestigationInput) => {
|
||||
const task = await createOperationalTask({
|
||||
@@ -297,6 +301,20 @@ export function useOperationalTasks(
|
||||
|
||||
const loadEvidenceSnapshot = useCallback((taskId: string, evidenceId: string) =>
|
||||
fetchOperationalTaskEvidenceSnapshot(taskId, evidenceId, getAccessToken), [getAccessToken]);
|
||||
const loadReports = useCallback((taskId: string, offset: number) => fetchOperationalTaskReports(taskId, offset, getAccessToken), [getAccessToken]);
|
||||
const markingReportsRef = useRef(new Set<string>());
|
||||
const markReportRead = useCallback((taskId: string, generation: number, revision: number) => {
|
||||
const unread = notifications.filter((item) => item.task_id === taskId && item.kind === "report_updated"
|
||||
&& item.lifecycle_status === "active" && item.status === "unread"
|
||||
&& item.report_reference?.execution_generation === generation && item.report_reference.revision <= revision
|
||||
&& !markingReportsRef.current.has(item.notification_id));
|
||||
for (const item of unread) {
|
||||
markingReportsRef.current.add(item.notification_id);
|
||||
void acknowledgeNotification(item.notification_id, item.version)
|
||||
.catch((error) => setError(error instanceof Error ? error.message : "新报告标记更新失败"))
|
||||
.finally(() => markingReportsRef.current.delete(item.notification_id));
|
||||
}
|
||||
}, [notifications, acknowledgeNotification]);
|
||||
|
||||
return {
|
||||
error: error ?? sync.error,
|
||||
@@ -313,7 +331,9 @@ export function useOperationalTasks(
|
||||
confirmWorkOrderDispatch,
|
||||
decideAccess,
|
||||
acknowledgeNotification,
|
||||
loadEvidenceSnapshot
|
||||
loadEvidenceSnapshot,
|
||||
loadReports,
|
||||
markReportRead
|
||||
};
|
||||
}
|
||||
|
||||
@@ -361,6 +381,8 @@ function toConditionStatus(task: OperationalTask): ScheduledConditionStatus {
|
||||
|
||||
function toRiskLevel(task: OperationalTask): ScheduledConditionRiskLevel {
|
||||
const { priority, lifecycle_status: status } = task;
|
||||
if (task.latest_report?.execution_generation === task.execution_generation
|
||||
&& task.latest_report?.report.outcome === "insufficient_data") return "attention";
|
||||
if (status === "failed" || priority === "urgent") return "critical";
|
||||
if (status === "waiting_human") return "attention";
|
||||
return "normal";
|
||||
|
||||
@@ -10,6 +10,8 @@ import { showMapNotice } from "@/features/map/core";
|
||||
import { findOwnedWorkbenchProject } from "../api/project-client";
|
||||
import {
|
||||
FrontendActionExecutor,
|
||||
agentTaskDiscussionContextSchema,
|
||||
type AgentTaskDiscussionContext,
|
||||
agentBootstrapKey,
|
||||
agentSessionsKey,
|
||||
createAgentApiClient,
|
||||
@@ -90,6 +92,7 @@ export function useWorkbenchAgent({
|
||||
const [sessionId, setSessionId] = useState<string | null>(null);
|
||||
const [sessionPurpose, setSessionPurpose] = useState<AgentSessionPurpose>("ordinary_chat");
|
||||
const [sessionTaskId, setSessionTaskId] = useState<string | null>(null);
|
||||
const taskDiscussionRef = useRef<{ sessionId: string; context: AgentTaskDiscussionContext } | null>(null);
|
||||
const [registry, setRegistry] = useState<UIRegistry | null>(null);
|
||||
const [sessionTitle, setSessionTitle] = useState("新对话");
|
||||
const [statusLabel, setStatusLabel] = useState("Agent 后端连接中");
|
||||
@@ -231,7 +234,11 @@ export function useWorkbenchAgent({
|
||||
api: `${env.TJWATER_AGENT_API_BASE_URL.replace(/\/$/, "")}/api/v1/agent/chat/stream`,
|
||||
headers: async (): Promise<Record<string, string>> => {
|
||||
const accessToken = await getAccessToken?.();
|
||||
return accessToken ? { Authorization: `Bearer ${accessToken}` } : {};
|
||||
const projectId = await getProjectId();
|
||||
return {
|
||||
...(projectId ? { "X-Project-Id": projectId } : {}),
|
||||
...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {})
|
||||
};
|
||||
},
|
||||
prepareSendMessagesRequest({ id, messages, body, trigger, messageId }) {
|
||||
return {
|
||||
@@ -248,7 +255,7 @@ export function useWorkbenchAgent({
|
||||
};
|
||||
}
|
||||
}),
|
||||
[getAccessToken]
|
||||
[getAccessToken, getProjectId]
|
||||
);
|
||||
|
||||
const chat = useChat<AgentUiMessage>({
|
||||
@@ -575,7 +582,7 @@ export function useWorkbenchAgent({
|
||||
[applySessionId, chat, refreshSessionHistory, startSessionStreamSubscription, stopSessionStreamSubscription, streaming]
|
||||
);
|
||||
|
||||
const openTaskCollaboration = useCallback(async (taskId: string) => {
|
||||
const openTaskCollaboration = useCallback(async (taskId: string, context?: AgentTaskDiscussionContext) => {
|
||||
const normalizedTaskId = taskId.trim();
|
||||
if (!normalizedTaskId) {
|
||||
throw new Error("工况任务 ID 不能为空。");
|
||||
@@ -586,6 +593,7 @@ export function useWorkbenchAgent({
|
||||
throw new Error("Agent 未返回任务协作会话。");
|
||||
}
|
||||
await loadHistorySession(collaborationSessionId);
|
||||
taskDiscussionRef.current = { sessionId: collaborationSessionId, context: agentTaskDiscussionContextSchema.parse(context ?? {}) };
|
||||
return collaborationSessionId;
|
||||
}, [loadHistorySession]);
|
||||
|
||||
@@ -709,7 +717,8 @@ export function useWorkbenchAgent({
|
||||
body: {
|
||||
session_id: sessionIdRef.current ?? undefined,
|
||||
model: selectedModel || undefined,
|
||||
approval_mode: approvalMode
|
||||
approval_mode: approvalMode,
|
||||
task_context: taskDiscussionRef.current?.sessionId === sessionIdRef.current ? taskDiscussionRef.current.context : undefined
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user