feat: polish web master-agent controls and dispatch recovery
This commit is contained in:
@@ -13,7 +13,7 @@ import {
|
||||
} from "@/components/app-ui";
|
||||
import { requirePageSession } from "@/lib/boss-auth";
|
||||
import { listDispatchPlansByProject, readState } from "@/lib/boss-data";
|
||||
import { latestPendingDispatchPlan, latestRejectedDispatchPlan } from "@/lib/dispatch-plan-ui";
|
||||
import { resolveDispatchPlanComposerState } from "@/lib/dispatch-plan-ui";
|
||||
import { formatTimestampLabel, getProjectDetailView } from "@/lib/boss-projections";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
@@ -27,13 +27,9 @@ export default async function ProjectChatPage({
|
||||
const { projectId } = await params;
|
||||
const state = await readState();
|
||||
const detail = getProjectDetailView(state, projectId, session.account);
|
||||
const pendingDispatchPlan = detail?.project.isGroup
|
||||
? latestPendingDispatchPlan(await listDispatchPlansByProject(projectId))
|
||||
: null;
|
||||
const rejectedDispatchPlan =
|
||||
detail?.project.isGroup && !pendingDispatchPlan
|
||||
? latestRejectedDispatchPlan(await listDispatchPlansByProject(projectId))
|
||||
: null;
|
||||
const dispatchPlanState = detail?.project.isGroup
|
||||
? resolveDispatchPlanComposerState(await listDispatchPlansByProject(projectId))
|
||||
: resolveDispatchPlanComposerState([]);
|
||||
|
||||
if (!detail) notFound();
|
||||
|
||||
@@ -169,12 +165,13 @@ export default async function ProjectChatPage({
|
||||
</div>
|
||||
<ChatComposer
|
||||
projectId={detail.project.id}
|
||||
dispatchPlanRecoveryHint={dispatchPlanState.pendingDispatchPlan ? dispatchPlanState.recoveryHint : null}
|
||||
initialPendingDispatchPlan={
|
||||
pendingDispatchPlan
|
||||
dispatchPlanState.pendingDispatchPlan
|
||||
? {
|
||||
planId: pendingDispatchPlan.planId,
|
||||
summary: pendingDispatchPlan.summary,
|
||||
targets: (pendingDispatchPlan.targets ?? []).map((target) => ({
|
||||
planId: dispatchPlanState.pendingDispatchPlan.planId,
|
||||
summary: dispatchPlanState.pendingDispatchPlan.summary,
|
||||
targets: (dispatchPlanState.pendingDispatchPlan.targets ?? []).map((target) => ({
|
||||
projectId: target.projectId,
|
||||
threadDisplayName: target.threadDisplayName,
|
||||
})),
|
||||
@@ -182,11 +179,11 @@ export default async function ProjectChatPage({
|
||||
: null
|
||||
}
|
||||
initialRejectedDispatchPlan={
|
||||
rejectedDispatchPlan
|
||||
dispatchPlanState.rejectedDispatchPlan
|
||||
? {
|
||||
planId: rejectedDispatchPlan.planId,
|
||||
summary: rejectedDispatchPlan.summary,
|
||||
targets: (rejectedDispatchPlan.targets ?? []).map((target) => ({
|
||||
planId: dispatchPlanState.rejectedDispatchPlan.planId,
|
||||
summary: dispatchPlanState.rejectedDispatchPlan.summary,
|
||||
targets: (dispatchPlanState.rejectedDispatchPlan.targets ?? []).map((target) => ({
|
||||
projectId: target.projectId,
|
||||
threadDisplayName: target.threadDisplayName,
|
||||
})),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { AppShell, PageNav, StatusBar } from "@/components/app-ui";
|
||||
import { MasterAgentPromptMemoryClient } from "@/components/master-agent-prompt-memory-client";
|
||||
import { requirePageSession } from "@/lib/boss-auth";
|
||||
import { MASTER_AGENT_CHAT_PAGE_ANCHORS } from "@/lib/master-agent-chat-menu";
|
||||
import {
|
||||
getMasterAgentPromptPolicy,
|
||||
getProjectAgentControls,
|
||||
@@ -43,6 +44,7 @@ export default async function MasterAgentPromptMemoryPage() {
|
||||
projectControls={projectControls}
|
||||
globalMemories={globalMemories}
|
||||
projectMemories={projectMemories}
|
||||
anchors={MASTER_AGENT_CHAT_PAGE_ANCHORS}
|
||||
/>
|
||||
</AppShell>
|
||||
);
|
||||
|
||||
@@ -867,10 +867,12 @@ export function ChatComposer({
|
||||
projectId,
|
||||
initialPendingDispatchPlan,
|
||||
initialRejectedDispatchPlan,
|
||||
dispatchPlanRecoveryHint,
|
||||
}: {
|
||||
projectId: string;
|
||||
initialPendingDispatchPlan?: PendingDispatchPlanState | null;
|
||||
initialRejectedDispatchPlan?: PendingDispatchPlanState | null;
|
||||
dispatchPlanRecoveryHint?: string | null;
|
||||
}) {
|
||||
const router = useRouter();
|
||||
const [value, setValue] = useState("");
|
||||
@@ -1128,6 +1130,11 @@ export function ChatComposer({
|
||||
{message}
|
||||
</div>
|
||||
) : null}
|
||||
{dispatchPlanRecoveryHint ? (
|
||||
<div className="mt-3 rounded-2xl border border-[#E5E5EA] bg-[#F7F8FA] px-4 py-3 text-[12px] leading-6 text-[#57606A]">
|
||||
{dispatchPlanRecoveryHint}
|
||||
</div>
|
||||
) : null}
|
||||
{pendingDispatchPlan ? (
|
||||
<div className="mt-3 rounded-2xl border border-[#E5E5EA] bg-[#F7F8FA] px-4 py-4 text-[12px] leading-6 text-[#57606A]">
|
||||
<div className="text-[14px] font-semibold text-[#111111]">等待你确认主 Agent 推荐</div>
|
||||
@@ -1167,7 +1174,10 @@ export function ChatComposer({
|
||||
<div className="mt-3 rounded-2xl border border-[#F3D19C] bg-[#FFF7E6] px-4 py-4 text-[12px] leading-6 text-[#8D5D00]">
|
||||
<div className="text-[14px] font-semibold text-[#111111]">上次推荐已拒绝</div>
|
||||
<div className="mt-2 whitespace-pre-line">{summarizeDispatchPlan(rejectedDispatchPlan)}</div>
|
||||
<div className="mt-2">如果还想继续当前协作,可以直接重新生成推荐,不用把整条需求重新打一遍。</div>
|
||||
<div className="mt-2">
|
||||
{dispatchPlanRecoveryHint ??
|
||||
"如果还想继续当前协作,可以直接重新生成新的推荐,不用把整条需求重新打一遍。"}
|
||||
</div>
|
||||
<div className="mt-3 flex flex-wrap gap-2">
|
||||
<button
|
||||
type="button"
|
||||
@@ -1175,7 +1185,7 @@ export function ChatComposer({
|
||||
onClick={() => void retryDispatchPlan()}
|
||||
className="rounded-full bg-[#07C160] px-4 py-2 text-[13px] font-semibold text-white disabled:bg-[#B7E6C9]"
|
||||
>
|
||||
重新生成推荐
|
||||
重新生成新的推荐
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -11,6 +11,7 @@ import type {
|
||||
ProjectAgentControls,
|
||||
UserMasterPrompt,
|
||||
} from "@/lib/boss-data";
|
||||
import type { MasterAgentChatPageAnchors } from "@/lib/master-agent-chat-menu";
|
||||
import { formatTimestampLabel } from "@/lib/boss-projections";
|
||||
|
||||
type MemoryDraft = {
|
||||
@@ -146,6 +147,7 @@ export function MasterAgentPromptMemoryClient({
|
||||
projectControls,
|
||||
globalMemories,
|
||||
projectMemories,
|
||||
anchors,
|
||||
}: {
|
||||
isAdmin: boolean;
|
||||
promptPolicy: MasterAgentPromptPolicy | null;
|
||||
@@ -153,6 +155,7 @@ export function MasterAgentPromptMemoryClient({
|
||||
projectControls: ProjectAgentControls | null;
|
||||
globalMemories: MasterAgentMemory[];
|
||||
projectMemories: MasterAgentMemory[];
|
||||
anchors: MasterAgentChatPageAnchors;
|
||||
}) {
|
||||
const router = useRouter();
|
||||
const [busyKey, setBusyKey] = useState<string | null>(null);
|
||||
@@ -326,7 +329,7 @@ export function MasterAgentPromptMemoryClient({
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4 px-[18px] pb-6">
|
||||
<div id="prompt-section" className="rounded-2xl border border-[#E5E5EA] bg-white px-4 py-4 scroll-mt-4">
|
||||
<div id={anchors.prompt.split("#")[1]} className="rounded-2xl border border-[#E5E5EA] bg-white px-4 py-4 scroll-mt-4">
|
||||
<div className="text-[16px] font-semibold text-[#111111]">主 Agent 提示词</div>
|
||||
<div className="mt-2 text-[12px] leading-6 text-[#8C8C8C]">
|
||||
管理员全局主提示词不可被覆盖;用户提示词和当前对话提示词只会追加在后面。
|
||||
@@ -400,7 +403,7 @@ export function MasterAgentPromptMemoryClient({
|
||||
</span>
|
||||
</div>
|
||||
<div className="grid gap-3 md:grid-cols-2">
|
||||
<label className="space-y-1">
|
||||
<label id={anchors.model.split("#")[1]} className="space-y-1 scroll-mt-4">
|
||||
<div className="text-[12px] text-[#8C8C8C]">模型</div>
|
||||
<select
|
||||
value={modelOverride}
|
||||
@@ -413,7 +416,7 @@ export function MasterAgentPromptMemoryClient({
|
||||
<option value="gpt-4.1-mini">gpt-4.1-mini</option>
|
||||
</select>
|
||||
</label>
|
||||
<label className="space-y-1">
|
||||
<label id={anchors.reasoningEffort.split("#")[1]} className="space-y-1 scroll-mt-4">
|
||||
<div className="text-[12px] text-[#8C8C8C]">推理强度</div>
|
||||
<select
|
||||
value={reasoningEffortOverride}
|
||||
@@ -453,7 +456,7 @@ export function MasterAgentPromptMemoryClient({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="memory-section" className="rounded-2xl border border-[#E5E5EA] bg-white px-4 py-4 scroll-mt-4">
|
||||
<div id={anchors.memory.split("#")[1]} className="rounded-2xl border border-[#E5E5EA] bg-white px-4 py-4 scroll-mt-4">
|
||||
<div className="text-[16px] font-semibold text-[#111111]">新增记忆</div>
|
||||
<div className="mt-2 text-[12px] leading-6 text-[#8C8C8C]">
|
||||
支持自动沉淀后的手动增补、编辑和归档。项目记忆需要绑定到真实项目,而不是 master-agent 会话本身。
|
||||
|
||||
@@ -18,6 +18,45 @@ export function latestRejectedDispatchPlan(plans: DispatchPlanUiPayload[] | null
|
||||
return (plans ?? []).find((plan) => plan.status === "rejected") ?? null;
|
||||
}
|
||||
|
||||
export type DispatchPlanComposerState = {
|
||||
pendingDispatchPlan: DispatchPlanUiPayload | null;
|
||||
rejectedDispatchPlan: DispatchPlanUiPayload | null;
|
||||
recoveryHint: string | null;
|
||||
recoveryActionLabel: string | null;
|
||||
};
|
||||
|
||||
export function resolveDispatchPlanComposerState(
|
||||
plans: DispatchPlanUiPayload[] | null | undefined,
|
||||
): DispatchPlanComposerState {
|
||||
const pendingDispatchPlan = latestPendingDispatchPlan(plans);
|
||||
const rejectedDispatchPlan = pendingDispatchPlan ? null : latestRejectedDispatchPlan(plans);
|
||||
|
||||
if (rejectedDispatchPlan) {
|
||||
return {
|
||||
pendingDispatchPlan,
|
||||
rejectedDispatchPlan,
|
||||
recoveryHint: "上次推荐已拒绝。直接点击“重新生成新的推荐”即可继续协作,不用重新发送整条消息。",
|
||||
recoveryActionLabel: "重新生成新的推荐",
|
||||
};
|
||||
}
|
||||
|
||||
if (pendingDispatchPlan) {
|
||||
return {
|
||||
pendingDispatchPlan,
|
||||
rejectedDispatchPlan: null,
|
||||
recoveryHint: "当前有待确认推荐,已折叠旧的拒绝状态。",
|
||||
recoveryActionLabel: null,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
pendingDispatchPlan: null,
|
||||
rejectedDispatchPlan: null,
|
||||
recoveryHint: null,
|
||||
recoveryActionLabel: null,
|
||||
};
|
||||
}
|
||||
|
||||
export function summarizeDispatchPlan(plan: DispatchPlanUiPayload | null | undefined) {
|
||||
if (!plan) {
|
||||
return "主 Agent 暂未生成推荐线程。";
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
export const MASTER_AGENT_CHAT_PAGE_ANCHORS = {
|
||||
prompt: "/me/master-agent#prompt-section",
|
||||
model: "/me/master-agent#model-section",
|
||||
reasoningEffort: "/me/master-agent#reasoning-effort-section",
|
||||
memory: "/me/master-agent#memory-section",
|
||||
} as const;
|
||||
|
||||
export type MasterAgentChatPageAnchors = typeof MASTER_AGENT_CHAT_PAGE_ANCHORS;
|
||||
|
||||
export type MasterAgentChatMenuItem = {
|
||||
key: "prompt" | "memory" | "refresh";
|
||||
key: "prompt" | "model" | "reasoning_effort" | "memory" | "refresh";
|
||||
label: string;
|
||||
href?: string;
|
||||
action?: "refresh";
|
||||
@@ -14,12 +23,22 @@ export function getMasterAgentChatMenuItems(projectId: string): MasterAgentChatM
|
||||
{
|
||||
key: "prompt",
|
||||
label: "提示词",
|
||||
href: "/me/master-agent#prompt-section",
|
||||
href: MASTER_AGENT_CHAT_PAGE_ANCHORS.prompt,
|
||||
},
|
||||
{
|
||||
key: "model",
|
||||
label: "模型",
|
||||
href: MASTER_AGENT_CHAT_PAGE_ANCHORS.model,
|
||||
},
|
||||
{
|
||||
key: "reasoning_effort",
|
||||
label: "推理强度",
|
||||
href: MASTER_AGENT_CHAT_PAGE_ANCHORS.reasoningEffort,
|
||||
},
|
||||
{
|
||||
key: "memory",
|
||||
label: "记忆",
|
||||
href: "/me/master-agent#memory-section",
|
||||
href: MASTER_AGENT_CHAT_PAGE_ANCHORS.memory,
|
||||
},
|
||||
{
|
||||
key: "refresh",
|
||||
|
||||
Reference in New Issue
Block a user