feat: 支持岗位会商任务和意图编辑
This commit is contained in:
1 parent
790267ac43
commit
7bd032ec77
2 files changed
+408
-12
No files matched your search
@@ -128,7 +128,7 @@ function toNode(node: ServerNode): PositionRoundtableNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function toSession(session: ServerSession): PositionRoundtableSession {
|
function toSession(session: ServerSession): PositionRoundtableSession {
|
||||||
return {
|
const result: PositionRoundtableSession = {
|
||||||
id: session.id,
|
id: session.id,
|
||||||
externalTaskId: session.external_task_id,
|
externalTaskId: session.external_task_id,
|
||||||
taskSnapshot: session.task_snapshot ?? {},
|
taskSnapshot: session.task_snapshot ?? {},
|
||||||
@@ -143,8 +143,11 @@ function toSession(session: ServerSession): PositionRoundtableSession {
|
|||||||
status: session.status,
|
status: session.status,
|
||||||
createdAt: session.created_at,
|
createdAt: session.created_at,
|
||||||
updatedAt: session.updated_at,
|
updatedAt: session.updated_at,
|
||||||
nodes: session.nodes?.map(toNode),
|
|
||||||
};
|
};
|
||||||
|
if (Array.isArray(session.nodes)) {
|
||||||
|
result.nodes = session.nodes.map(toNode);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function listPositionSessions(externalTaskId?: string): Promise<PositionRoundtableSession[]> {
|
export async function listPositionSessions(externalTaskId?: string): Promise<PositionRoundtableSession[]> {
|
||||||
@@ -182,6 +185,21 @@ export async function getPositionSession(sessionId: string): Promise<PositionRou
|
|||||||
return toSession(result);
|
return toSession(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function updatePositionTaskSnapshot(
|
||||||
|
sessionId: string,
|
||||||
|
taskSnapshot: Record<string, unknown>,
|
||||||
|
): Promise<PositionRoundtableSession> {
|
||||||
|
const result = await readJson<ServerSession>(
|
||||||
|
await apiFetch(`${BASE()}/sessions/${encodeURIComponent(sessionId)}/task`, {
|
||||||
|
method: "PUT",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ task_snapshot: taskSnapshot }),
|
||||||
|
}),
|
||||||
|
"update position task",
|
||||||
|
);
|
||||||
|
return toSession(result);
|
||||||
|
}
|
||||||
|
|
||||||
export async function savePositionIntent(
|
export async function savePositionIntent(
|
||||||
sessionId: string,
|
sessionId: string,
|
||||||
input: { intentThreadId?: string | null; intentSnapshot: object | null },
|
input: { intentThreadId?: string | null; intentSnapshot: object | null },
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
Layers3,
|
Layers3,
|
||||||
Loader2,
|
Loader2,
|
||||||
MessageSquareText,
|
MessageSquareText,
|
||||||
|
PencilLine,
|
||||||
Plus,
|
Plus,
|
||||||
RotateCcw,
|
RotateCcw,
|
||||||
Search,
|
Search,
|
||||||
@@ -86,6 +87,7 @@ import {
|
|||||||
preparePositionSummary,
|
preparePositionSummary,
|
||||||
rejectPositionNode,
|
rejectPositionNode,
|
||||||
savePositionIntent,
|
savePositionIntent,
|
||||||
|
updatePositionTaskSnapshot,
|
||||||
type PositionRoundtableNode,
|
type PositionRoundtableNode,
|
||||||
type PositionRoundtableSession,
|
type PositionRoundtableSession,
|
||||||
} from "@/position-roundtable/api/sessions";
|
} from "@/position-roundtable/api/sessions";
|
||||||
@@ -116,6 +118,11 @@ type PositionArtifactPreview = {
|
|||||||
mimeType: string | null;
|
mimeType: string | null;
|
||||||
auto?: boolean;
|
auto?: boolean;
|
||||||
};
|
};
|
||||||
|
type IntentEditDraft = {
|
||||||
|
objective: string;
|
||||||
|
constraints: string;
|
||||||
|
assumptions: string;
|
||||||
|
};
|
||||||
|
|
||||||
function sandboxThreadSignature(thread: BaseStream<AgentThreadState>): string {
|
function sandboxThreadSignature(thread: BaseStream<AgentThreadState>): string {
|
||||||
const messages = thread.messages ?? [];
|
const messages = thread.messages ?? [];
|
||||||
@@ -347,6 +354,14 @@ function artifactName(artifact: Record<string, unknown> | null | undefined): str
|
|||||||
return path.split("/").filter(Boolean).at(-1) ?? "";
|
return path.split("/").filter(Boolean).at(-1) ?? "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function mergePositionSession(
|
||||||
|
current: PositionRoundtableSession | null,
|
||||||
|
next: PositionRoundtableSession,
|
||||||
|
): PositionRoundtableSession {
|
||||||
|
if (!current || current.id !== next.id || Array.isArray(next.nodes)) return next;
|
||||||
|
return { ...current, ...next, nodes: current.nodes };
|
||||||
|
}
|
||||||
|
|
||||||
function parseIntentSummary(snapshot: Record<string, unknown> | null | undefined): IntentSummary | null {
|
function parseIntentSummary(snapshot: Record<string, unknown> | null | undefined): IntentSummary | null {
|
||||||
if (!snapshot || typeof snapshot.objective !== "string" || !Array.isArray(snapshot.constraints)) {
|
if (!snapshot || typeof snapshot.objective !== "string" || !Array.isArray(snapshot.constraints)) {
|
||||||
return null;
|
return null;
|
||||||
@@ -430,6 +445,17 @@ export default function PositionRoundtablePage({
|
|||||||
signature: string;
|
signature: string;
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
const [flowDetailItem, setFlowDetailItem] = useState<ResultItem | null>(null);
|
const [flowDetailItem, setFlowDetailItem] = useState<ResultItem | null>(null);
|
||||||
|
const [taskEditorOpen, setTaskEditorOpen] = useState(false);
|
||||||
|
const [taskDraft, setTaskDraft] = useState<PositionTask>(fallbackTask);
|
||||||
|
const [taskSaving, setTaskSaving] = useState(false);
|
||||||
|
const [intentEditorOpen, setIntentEditorOpen] = useState(false);
|
||||||
|
const [intentDraft, setIntentDraft] = useState<IntentEditDraft>({
|
||||||
|
objective: "",
|
||||||
|
constraints: "",
|
||||||
|
assumptions: "",
|
||||||
|
});
|
||||||
|
const [intentSaving, setIntentSaving] = useState(false);
|
||||||
|
const [intentCompletedAt, setIntentCompletedAt] = useState("");
|
||||||
const activePosition = POSITIONS.find((item) => item.id === positionId)!;
|
const activePosition = POSITIONS.find((item) => item.id === positionId)!;
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
artifactsCtxRef.current = artifactsCtx;
|
artifactsCtxRef.current = artifactsCtx;
|
||||||
@@ -482,6 +508,140 @@ export default function PositionRoundtablePage({
|
|||||||
});
|
});
|
||||||
const hydratedIntentSessionRef = useRef<string | null>(null);
|
const hydratedIntentSessionRef = useRef<string | null>(null);
|
||||||
const persistedIntentRef = useRef<string | null>(null);
|
const persistedIntentRef = useRef<string | null>(null);
|
||||||
|
const isArchivedSession = positionSession?.status === "archived";
|
||||||
|
|
||||||
|
const syncSessionUrl = useCallback(
|
||||||
|
(sessionId: string, nextTaskId: string) => {
|
||||||
|
const next = new URLSearchParams(searchParamsString);
|
||||||
|
next.set("taskId", nextTaskId);
|
||||||
|
next.set("sessionId", sessionId);
|
||||||
|
next.delete("new");
|
||||||
|
setSearchParams(next, { replace: true });
|
||||||
|
},
|
||||||
|
[searchParamsString, setSearchParams],
|
||||||
|
);
|
||||||
|
|
||||||
|
const ensureEditableSession = useCallback(
|
||||||
|
async (taskSnapshot: PositionTask): Promise<PositionRoundtableSession> => {
|
||||||
|
if (positionSession) return positionSession;
|
||||||
|
const created = await createPositionSession({
|
||||||
|
externalTaskId: taskSnapshot.id || undefined,
|
||||||
|
taskSnapshot: { ...taskSnapshot },
|
||||||
|
});
|
||||||
|
setPositionSession(created);
|
||||||
|
syncSessionUrl(created.id, taskSnapshot.id);
|
||||||
|
return created;
|
||||||
|
},
|
||||||
|
[positionSession, syncSessionUrl],
|
||||||
|
);
|
||||||
|
|
||||||
|
const openTaskEditor = useCallback(() => {
|
||||||
|
if (isArchivedSession) return;
|
||||||
|
setTaskDraft({ ...task });
|
||||||
|
setTaskEditorOpen(true);
|
||||||
|
}, [isArchivedSession, task]);
|
||||||
|
|
||||||
|
const handleSaveTaskEdit = useCallback(async () => {
|
||||||
|
if (isArchivedSession) {
|
||||||
|
toast.warning("已归档的会话不能编辑任务信息");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const title = taskDraft.title.trim();
|
||||||
|
if (!title) {
|
||||||
|
toast.warning("请填写任务名称");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const nextTask: PositionTask = {
|
||||||
|
...task,
|
||||||
|
title,
|
||||||
|
description: taskDraft.description.trim(),
|
||||||
|
direction: taskDraft.direction.trim() || task.direction,
|
||||||
|
};
|
||||||
|
setTaskSaving(true);
|
||||||
|
try {
|
||||||
|
const saved = positionSession
|
||||||
|
? await updatePositionTaskSnapshot(positionSession.id, { ...nextTask })
|
||||||
|
: await createPositionSession({
|
||||||
|
externalTaskId: nextTask.id || undefined,
|
||||||
|
taskSnapshot: { ...nextTask },
|
||||||
|
});
|
||||||
|
setPositionSession((current) => mergePositionSession(current, saved));
|
||||||
|
syncSessionUrl(saved.id, nextTask.id);
|
||||||
|
setTaskEditorOpen(false);
|
||||||
|
toast.success("任务信息已更新");
|
||||||
|
} catch (error) {
|
||||||
|
toast.error(`任务信息保存失败:${error instanceof Error ? error.message : String(error)}`);
|
||||||
|
} finally {
|
||||||
|
setTaskSaving(false);
|
||||||
|
}
|
||||||
|
}, [isArchivedSession, positionSession, syncSessionUrl, task, taskDraft]);
|
||||||
|
|
||||||
|
const openIntentEditor = useCallback(() => {
|
||||||
|
if (isArchivedSession) return;
|
||||||
|
const currentIntent = frozenIntent ?? intent.intentReady;
|
||||||
|
setIntentDraft({
|
||||||
|
objective: currentIntent?.objective ?? "",
|
||||||
|
constraints: (currentIntent?.constraints ?? []).join("\n"),
|
||||||
|
assumptions: (currentIntent?.assumptions ?? []).join("\n"),
|
||||||
|
});
|
||||||
|
setIntentEditorOpen(true);
|
||||||
|
}, [frozenIntent, intent.intentReady, isArchivedSession]);
|
||||||
|
|
||||||
|
const handleSaveIntentEdit = useCallback(async () => {
|
||||||
|
if (isArchivedSession) {
|
||||||
|
toast.warning("已归档的会话不能编辑意图理解");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const objective = intentDraft.objective.trim();
|
||||||
|
if (!objective) {
|
||||||
|
toast.warning("请填写任务意图");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const splitLines = (value: string) =>
|
||||||
|
value
|
||||||
|
.split(/\r?\n/)
|
||||||
|
.map((item) => item.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
const nextIntent: IntentSummary = {
|
||||||
|
objective,
|
||||||
|
constraints: splitLines(intentDraft.constraints),
|
||||||
|
assumptions: splitLines(intentDraft.assumptions),
|
||||||
|
};
|
||||||
|
|
||||||
|
setIntentSaving(true);
|
||||||
|
try {
|
||||||
|
const baseSession = await ensureEditableSession(task);
|
||||||
|
const intentThreadId = baseSession.intentThreadId ?? intent.intentThreadId;
|
||||||
|
const saved = await savePositionIntent(baseSession.id, {
|
||||||
|
intentThreadId,
|
||||||
|
intentSnapshot: nextIntent,
|
||||||
|
});
|
||||||
|
persistedIntentRef.current = `${saved.id}:${intentThreadId ?? ""}:${JSON.stringify(nextIntent)}`;
|
||||||
|
setPositionSession((current) => mergePositionSession(current, saved));
|
||||||
|
intent.hydrateFromDraft({
|
||||||
|
intentThreadId: intentThreadId ?? intent.intentThreadId,
|
||||||
|
intentReady: nextIntent,
|
||||||
|
interactiveMessages: intent.interactiveMessages,
|
||||||
|
timelineEntries: [],
|
||||||
|
});
|
||||||
|
if (!intentCompletedAt) setIntentCompletedAt(timeLabel());
|
||||||
|
setIntentEditorOpen(false);
|
||||||
|
toast.success("意图理解已更新");
|
||||||
|
} catch (error) {
|
||||||
|
toast.error(`意图理解保存失败:${error instanceof Error ? error.message : String(error)}`);
|
||||||
|
} finally {
|
||||||
|
setIntentSaving(false);
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
ensureEditableSession,
|
||||||
|
intent,
|
||||||
|
intentCompletedAt,
|
||||||
|
intentDraft.assumptions,
|
||||||
|
intentDraft.constraints,
|
||||||
|
intentDraft.objective,
|
||||||
|
isArchivedSession,
|
||||||
|
task,
|
||||||
|
]);
|
||||||
|
|
||||||
const { data: chains = [], isLoading: chainsLoading } = useQuery({
|
const { data: chains = [], isLoading: chainsLoading } = useQuery({
|
||||||
queryKey: ["position-roundtable", "chains"],
|
queryKey: ["position-roundtable", "chains"],
|
||||||
@@ -550,6 +710,25 @@ export default function PositionRoundtablePage({
|
|||||||
const nextNodeKey = activeNode?.nodeKey ?? null;
|
const nextNodeKey = activeNode?.nodeKey ?? null;
|
||||||
if (activeNodeKey !== nextNodeKey) setActiveNodeKey(nextNodeKey);
|
if (activeNodeKey !== nextNodeKey) setActiveNodeKey(nextNodeKey);
|
||||||
}, [activeNode?.nodeKey, activeNodeKey]);
|
}, [activeNode?.nodeKey, activeNodeKey]);
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
!sessionIdFromUrl ||
|
||||||
|
!positionSession ||
|
||||||
|
Array.isArray(positionSession.nodes) ||
|
||||||
|
positionSession.status === "intent_pending"
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let cancelled = false;
|
||||||
|
void getPositionSession(sessionIdFromUrl)
|
||||||
|
.then((session) => {
|
||||||
|
if (!cancelled) setPositionSession(session);
|
||||||
|
})
|
||||||
|
.catch(() => undefined);
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, [positionSession, sessionIdFromUrl]);
|
||||||
const handleNodeUpdate = useCallback((updatedNode: PositionRoundtableNode) => {
|
const handleNodeUpdate = useCallback((updatedNode: PositionRoundtableNode) => {
|
||||||
setPositionSession((current) => {
|
setPositionSession((current) => {
|
||||||
if (!current?.nodes) return current;
|
if (!current?.nodes) return current;
|
||||||
@@ -597,7 +776,7 @@ export default function PositionRoundtablePage({
|
|||||||
});
|
});
|
||||||
if (cancelled) return;
|
if (cancelled) return;
|
||||||
persistedIntentRef.current = `${saved.id}:${intentThreadId}:${snapshotKey}`;
|
persistedIntentRef.current = `${saved.id}:${intentThreadId}:${snapshotKey}`;
|
||||||
setPositionSession((current) => (current?.id === saved.id ? { ...current, ...saved } : saved));
|
setPositionSession((current) => mergePositionSession(current, saved));
|
||||||
if (!sessionIdFromUrl) {
|
if (!sessionIdFromUrl) {
|
||||||
const next = new URLSearchParams(searchParamsString);
|
const next = new URLSearchParams(searchParamsString);
|
||||||
next.set("taskId", task.id);
|
next.set("taskId", task.id);
|
||||||
@@ -756,7 +935,6 @@ export default function PositionRoundtablePage({
|
|||||||
}, [sessionIdFromUrl]);
|
}, [sessionIdFromUrl]);
|
||||||
|
|
||||||
const { agent: selectedAgent } = useAgent(selectedAgentId);
|
const { agent: selectedAgent } = useAgent(selectedAgentId);
|
||||||
const [intentCompletedAt, setIntentCompletedAt] = useState("");
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (intent.intentReady && !intentCompletedAt) setIntentCompletedAt(timeLabel());
|
if (intent.intentReady && !intentCompletedAt) setIntentCompletedAt(timeLabel());
|
||||||
if (!intent.intentReady && intentCompletedAt) setIntentCompletedAt("");
|
if (!intent.intentReady && intentCompletedAt) setIntentCompletedAt("");
|
||||||
@@ -857,7 +1035,7 @@ export default function PositionRoundtablePage({
|
|||||||
if (!window.confirm("归档后该会话将只读,是否继续?")) return;
|
if (!window.confirm("归档后该会话将只读,是否继续?")) return;
|
||||||
void archivePositionSession(positionSession.id)
|
void archivePositionSession(positionSession.id)
|
||||||
.then((session) => {
|
.then((session) => {
|
||||||
setPositionSession((current) => (current ? { ...current, ...session } : session));
|
setPositionSession((current) => mergePositionSession(current, session));
|
||||||
toast.success("会话已归档,可新建会话继续研讨");
|
toast.success("会话已归档,可新建会话继续研讨");
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
@@ -1163,11 +1341,19 @@ export default function PositionRoundtablePage({
|
|||||||
|
|
||||||
<div className="flex min-h-0 flex-1 gap-3 overflow-hidden p-3">
|
<div className="flex min-h-0 flex-1 gap-3 overflow-hidden p-3">
|
||||||
<aside className="custom-scrollbar flex w-[360px] shrink-0 flex-col gap-3 overflow-y-auto rounded-2xl border border-slate-200 bg-white p-3 shadow-sm dark:border-[#2b8ffe]/45 dark:bg-[#032650]/80 dark:text-[#c5cfe0] dark:shadow-none dark:backdrop-blur-md">
|
<aside className="custom-scrollbar flex w-[360px] shrink-0 flex-col gap-3 overflow-y-auto rounded-2xl border border-slate-200 bg-white p-3 shadow-sm dark:border-[#2b8ffe]/45 dark:bg-[#032650]/80 dark:text-[#c5cfe0] dark:shadow-none dark:backdrop-blur-md">
|
||||||
<TaskInformationCard task={task} />
|
<TaskInformationCard
|
||||||
|
task={task}
|
||||||
|
editable={!isArchivedSession}
|
||||||
|
onEdit={openTaskEditor}
|
||||||
|
/>
|
||||||
|
|
||||||
{activePosition.isIntentPosition ? (
|
{activePosition.isIntentPosition ? (
|
||||||
isIntelligenceDeliveryOverview ? (
|
isIntelligenceDeliveryOverview ? (
|
||||||
<IntentUnderstandingCard intent={frozenIntent ?? intent.intentReady} />
|
<IntentUnderstandingCard
|
||||||
|
intent={frozenIntent ?? intent.intentReady}
|
||||||
|
editable={!isArchivedSession}
|
||||||
|
onEdit={openIntentEditor}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<BusinessChainSelector
|
<BusinessChainSelector
|
||||||
chains={chains}
|
chains={chains}
|
||||||
@@ -1179,7 +1365,11 @@ export default function PositionRoundtablePage({
|
|||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
<div className="contents">
|
<div className="contents">
|
||||||
<IntentUnderstandingCard intent={intent.intentReady} />
|
<IntentUnderstandingCard
|
||||||
|
intent={frozenIntent ?? intent.intentReady}
|
||||||
|
editable={!isArchivedSession}
|
||||||
|
onEdit={openIntentEditor}
|
||||||
|
/>
|
||||||
<BusinessChainFlow
|
<BusinessChainFlow
|
||||||
chain={activeChain}
|
chain={activeChain}
|
||||||
nodes={positionSession?.nodes ?? []}
|
nodes={positionSession?.nodes ?? []}
|
||||||
@@ -1368,6 +1558,22 @@ export default function PositionRoundtablePage({
|
|||||||
showReject={false}
|
showReject={false}
|
||||||
showBuiltinAsk={false}
|
showBuiltinAsk={false}
|
||||||
/>
|
/>
|
||||||
|
<TaskInformationEditDialog
|
||||||
|
open={taskEditorOpen}
|
||||||
|
task={taskDraft}
|
||||||
|
saving={taskSaving}
|
||||||
|
onOpenChange={setTaskEditorOpen}
|
||||||
|
onChange={setTaskDraft}
|
||||||
|
onSubmit={handleSaveTaskEdit}
|
||||||
|
/>
|
||||||
|
<IntentUnderstandingEditDialog
|
||||||
|
open={intentEditorOpen}
|
||||||
|
draft={intentDraft}
|
||||||
|
saving={intentSaving}
|
||||||
|
onOpenChange={setIntentEditorOpen}
|
||||||
|
onChange={setIntentDraft}
|
||||||
|
onSubmit={handleSaveIntentEdit}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -1732,10 +1938,39 @@ function DeliveryOverviewCard({ item }: { item: DeliveryOverviewItem }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 仅展示岗位会商任务,不在会商页编辑任务。 */
|
/** 岗位会商内的任务快照展示,可按当前 Session 独立编辑。 */
|
||||||
function TaskInformationCard({ task }: { task: PositionTask }) {
|
function HoverEditButton({
|
||||||
|
title,
|
||||||
|
onClick,
|
||||||
|
}: {
|
||||||
|
title: string;
|
||||||
|
onClick: () => void;
|
||||||
|
}) {
|
||||||
return (
|
return (
|
||||||
<section className="rounded-2xl border border-[#ECE7E4] bg-white p-4 shadow-[0_4px_20px_rgba(0,0,0,0.02)] dark:border-[#2b8ffe]/30 dark:bg-[#042b59]/55 dark:shadow-none">
|
<button
|
||||||
|
type="button"
|
||||||
|
title={title}
|
||||||
|
aria-label={title}
|
||||||
|
onClick={onClick}
|
||||||
|
className="absolute right-3 top-3 z-10 flex size-7 items-center justify-center rounded-full border border-slate-200 bg-white/95 text-slate-500 opacity-0 shadow-sm transition-all hover:border-primary/40 hover:bg-primary/5 hover:text-primary focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/30 group-hover:opacity-100 dark:border-[#2b8ffe]/35 dark:bg-[#063363]/95 dark:text-[#9ebfe2] dark:hover:border-[#2b8ffe]/80 dark:hover:bg-[#0b4fae]/45 dark:hover:text-[#83c1ff]"
|
||||||
|
>
|
||||||
|
<PencilLine className="size-3.5" strokeWidth={2.4} />
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function TaskInformationCard({
|
||||||
|
task,
|
||||||
|
editable,
|
||||||
|
onEdit,
|
||||||
|
}: {
|
||||||
|
task: PositionTask;
|
||||||
|
editable?: boolean;
|
||||||
|
onEdit?: () => void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<section className="group relative rounded-2xl border border-[#ECE7E4] bg-white p-4 shadow-[0_4px_20px_rgba(0,0,0,0.02)] dark:border-[#2b8ffe]/30 dark:bg-[#042b59]/55 dark:shadow-none">
|
||||||
|
{editable && onEdit && <HoverEditButton title="编辑任务信息" onClick={onEdit} />}
|
||||||
<div className="mb-3 flex items-center gap-2">
|
<div className="mb-3 flex items-center gap-2">
|
||||||
<span className="flex size-5 items-center justify-center rounded-md bg-primary text-primary-foreground">
|
<span className="flex size-5 items-center justify-center rounded-md bg-primary text-primary-foreground">
|
||||||
<Target className="size-3" strokeWidth={2.5} />
|
<Target className="size-3" strokeWidth={2.5} />
|
||||||
@@ -1774,6 +2009,72 @@ function TaskInformationCard({ task }: { task: PositionTask }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function TaskInformationEditDialog({
|
||||||
|
open,
|
||||||
|
task,
|
||||||
|
saving,
|
||||||
|
onOpenChange,
|
||||||
|
onChange,
|
||||||
|
onSubmit,
|
||||||
|
}: {
|
||||||
|
open: boolean;
|
||||||
|
task: PositionTask;
|
||||||
|
saving: boolean;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
onChange: (task: PositionTask) => void;
|
||||||
|
onSubmit: () => void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
open={open}
|
||||||
|
onOpenChange={(nextOpen) => {
|
||||||
|
if (!saving) onOpenChange(nextOpen);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DialogContent className="sm:max-w-[560px] dark:border-[#2b8ffe]/35 dark:bg-[#042b59]">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>编辑任务信息</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="space-y-4 py-1">
|
||||||
|
<label className="block space-y-1.5">
|
||||||
|
<span className="text-xs font-semibold text-slate-500 dark:text-[#9ebfe2]">任务名称</span>
|
||||||
|
<TInput
|
||||||
|
value={task.title}
|
||||||
|
onChange={(value) => onChange({ ...task, title: String(value) })}
|
||||||
|
placeholder="请输入任务名称"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label className="block space-y-1.5">
|
||||||
|
<span className="text-xs font-semibold text-slate-500 dark:text-[#9ebfe2]">任务描述</span>
|
||||||
|
<Textarea
|
||||||
|
value={task.description}
|
||||||
|
onChange={(event) => onChange({ ...task, description: event.target.value })}
|
||||||
|
className="min-h-28 resize-none dark:border-[#2b8ffe]/35 dark:bg-[#032650]/80"
|
||||||
|
placeholder="请输入任务描述"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label className="block space-y-1.5">
|
||||||
|
<span className="text-xs font-semibold text-slate-500 dark:text-[#9ebfe2]">任务方向</span>
|
||||||
|
<TInput
|
||||||
|
value={task.direction}
|
||||||
|
onChange={(value) => onChange({ ...task, direction: String(value) })}
|
||||||
|
placeholder="请输入任务方向"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<DialogFooter>
|
||||||
|
<TButton variant="outline" disabled={saving} onClick={() => onOpenChange(false)}>
|
||||||
|
取消
|
||||||
|
</TButton>
|
||||||
|
<TButton theme="primary" loading={saving} onClick={onSubmit}>
|
||||||
|
保存
|
||||||
|
</TButton>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function BusinessChainSelector({
|
function BusinessChainSelector({
|
||||||
chains,
|
chains,
|
||||||
loading,
|
loading,
|
||||||
@@ -1976,11 +2277,16 @@ function BusinessChainSelector({
|
|||||||
|
|
||||||
function IntentUnderstandingCard({
|
function IntentUnderstandingCard({
|
||||||
intent,
|
intent,
|
||||||
|
editable,
|
||||||
|
onEdit,
|
||||||
}: {
|
}: {
|
||||||
intent: ReturnType<typeof useStep1Intent>["intentReady"];
|
intent: ReturnType<typeof useStep1Intent>["intentReady"];
|
||||||
|
editable?: boolean;
|
||||||
|
onEdit?: () => void;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<section className="rounded-2xl border border-[#ECE7E4] bg-white p-4 shadow-[0_4px_20px_rgba(0,0,0,0.02)] dark:border-[#2b8ffe]/30 dark:bg-[#042b59]/55 dark:shadow-none">
|
<section className="group relative rounded-2xl border border-[#ECE7E4] bg-white p-4 shadow-[0_4px_20px_rgba(0,0,0,0.02)] dark:border-[#2b8ffe]/30 dark:bg-[#042b59]/55 dark:shadow-none">
|
||||||
|
{editable && onEdit && <HoverEditButton title="编辑意图理解" onClick={onEdit} />}
|
||||||
<h2 className="mb-3 flex items-center gap-2 text-sm font-semibold tracking-wide text-slate-900 dark:text-[#e8edf5]">
|
<h2 className="mb-3 flex items-center gap-2 text-sm font-semibold tracking-wide text-slate-900 dark:text-[#e8edf5]">
|
||||||
<span className="flex size-5 items-center justify-center rounded-md bg-primary text-primary-foreground">
|
<span className="flex size-5 items-center justify-center rounded-md bg-primary text-primary-foreground">
|
||||||
<Sparkles className="size-3" strokeWidth={2.5} />
|
<Sparkles className="size-3" strokeWidth={2.5} />
|
||||||
@@ -2011,6 +2317,78 @@ function IntentUnderstandingCard({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function IntentUnderstandingEditDialog({
|
||||||
|
open,
|
||||||
|
draft,
|
||||||
|
saving,
|
||||||
|
onOpenChange,
|
||||||
|
onChange,
|
||||||
|
onSubmit,
|
||||||
|
}: {
|
||||||
|
open: boolean;
|
||||||
|
draft: IntentEditDraft;
|
||||||
|
saving: boolean;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
onChange: (draft: IntentEditDraft) => void;
|
||||||
|
onSubmit: () => void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
open={open}
|
||||||
|
onOpenChange={(nextOpen) => {
|
||||||
|
if (!saving) onOpenChange(nextOpen);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DialogContent className="sm:max-w-[600px] dark:border-[#2b8ffe]/35 dark:bg-[#042b59]">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>编辑意图理解</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="space-y-4 py-1">
|
||||||
|
<label className="block space-y-1.5">
|
||||||
|
<span className="text-xs font-semibold text-slate-500 dark:text-[#9ebfe2]">任务意图</span>
|
||||||
|
<Textarea
|
||||||
|
value={draft.objective}
|
||||||
|
onChange={(event) => onChange({ ...draft, objective: event.target.value })}
|
||||||
|
className="min-h-24 resize-none dark:border-[#2b8ffe]/35 dark:bg-[#032650]/80"
|
||||||
|
placeholder="请输入识别后的任务意图"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label className="block space-y-1.5">
|
||||||
|
<span className="text-xs font-semibold text-slate-500 dark:text-[#9ebfe2]">
|
||||||
|
关键约束
|
||||||
|
</span>
|
||||||
|
<Textarea
|
||||||
|
value={draft.constraints}
|
||||||
|
onChange={(event) => onChange({ ...draft, constraints: event.target.value })}
|
||||||
|
className="min-h-28 resize-none dark:border-[#2b8ffe]/35 dark:bg-[#032650]/80"
|
||||||
|
placeholder="每行一条关键约束"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label className="block space-y-1.5">
|
||||||
|
<span className="text-xs font-semibold text-slate-500 dark:text-[#9ebfe2]">
|
||||||
|
补充假设 / 说明
|
||||||
|
</span>
|
||||||
|
<Textarea
|
||||||
|
value={draft.assumptions}
|
||||||
|
onChange={(event) => onChange({ ...draft, assumptions: event.target.value })}
|
||||||
|
className="min-h-20 resize-none dark:border-[#2b8ffe]/35 dark:bg-[#032650]/80"
|
||||||
|
placeholder="可选,每行一条"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<DialogFooter>
|
||||||
|
<TButton variant="outline" disabled={saving} onClick={() => onOpenChange(false)}>
|
||||||
|
取消
|
||||||
|
</TButton>
|
||||||
|
<TButton theme="primary" loading={saving} onClick={onSubmit}>
|
||||||
|
保存
|
||||||
|
</TButton>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function chainStages(chain: BusinessChain) {
|
function chainStages(chain: BusinessChain) {
|
||||||
const seatById = new Map(chain.seats.map((seat) => [seat.agent_id, seat]));
|
const seatById = new Map(chain.seats.map((seat) => [seat.agent_id, seat]));
|
||||||
if (chain.stages?.length) {
|
if (chain.stages?.length) {
|
||||||
|
|||||||
Reference in new issue
Block a user