Compare commits
No commits in common. "bcc3d9e00e3bc4e7b7643f61cf33dd07a6cdd691" and "3951a9cbc5acecb0fb7cadb622e0d4ae35faa1db" have entirely different histories.
bcc3d9e00e
...
3951a9cbc5
|
|
@ -4387,7 +4387,6 @@ function SeoAnchorReviewWorkspace({
|
|||
isCollapsed,
|
||||
isBulkApproving,
|
||||
isDemandProfileDirty,
|
||||
isReadOnly,
|
||||
isSavingDraft,
|
||||
isManualAdding,
|
||||
onDecision,
|
||||
|
|
@ -4411,7 +4410,6 @@ function SeoAnchorReviewWorkspace({
|
|||
isCollapsed: boolean;
|
||||
isBulkApproving: boolean;
|
||||
isDemandProfileDirty: boolean;
|
||||
isReadOnly: boolean;
|
||||
isSavingDraft: boolean;
|
||||
isManualAdding: boolean;
|
||||
onDecision: (anchor: AnchorReviewAnchorView, decision: AnchorReviewDecisionInput) => void;
|
||||
|
|
@ -4449,11 +4447,7 @@ function SeoAnchorReviewWorkspace({
|
|||
<small>Выбираем смысловые якоря и вручную решаем, что отправлять в очередь Wordstat.</small>
|
||||
<small className="keyword-stage-profile-line">
|
||||
Текущий активный профиль: <strong>{contextProfileLabel}</strong>
|
||||
{contextProfileStatus === "loading"
|
||||
? " · загружается"
|
||||
: contextProfileStatus === "snapshot"
|
||||
? " · снимок только для просмотра"
|
||||
: ""}
|
||||
{contextProfileStatus === "loading" ? " · загружается" : contextProfileStatus === "snapshot" ? " · снимок" : ""}
|
||||
</small>
|
||||
<small>
|
||||
{getAnchorReviewStateLabel(anchorReview.state)} · очередь Wordstat {anchorReview.readiness.wordstatQueueCount} ·
|
||||
|
|
@ -4468,21 +4462,12 @@ function SeoAnchorReviewWorkspace({
|
|||
<button
|
||||
aria-label="Сохранить раскладку семантического базиса"
|
||||
className={`keyword-stage-icon-action${draftChangeCount > 0 ? " active" : ""}`}
|
||||
disabled={
|
||||
isReadOnly ||
|
||||
isBulkApproving ||
|
||||
isManualAdding ||
|
||||
isSavingDraft ||
|
||||
draftChangeCount === 0 ||
|
||||
anchorReview.state === "not_ready"
|
||||
}
|
||||
disabled={isBulkApproving || isManualAdding || isSavingDraft || draftChangeCount === 0 || anchorReview.state === "not_ready"}
|
||||
onClick={onSaveDraft}
|
||||
title={
|
||||
isReadOnly
|
||||
? "Snapshot открыт только для просмотра: переключись на текущий контекст, чтобы редактировать Stage 1"
|
||||
: draftChangeCount > 0
|
||||
? `Сохранить ${draftChangeCount} изменений Stage 1`
|
||||
: "Нет несохранённых изменений"
|
||||
draftChangeCount > 0
|
||||
? `Сохранить ${draftChangeCount} изменений Stage 1`
|
||||
: "Нет несохранённых изменений"
|
||||
}
|
||||
type="button"
|
||||
>
|
||||
|
|
@ -4491,13 +4476,9 @@ function SeoAnchorReviewWorkspace({
|
|||
<button
|
||||
aria-label="Ручное добавление ключа"
|
||||
className="keyword-stage-icon-action"
|
||||
disabled={isReadOnly || isBulkApproving || isManualAdding || anchorReview.state === "not_ready"}
|
||||
disabled={isBulkApproving || isManualAdding || anchorReview.state === "not_ready"}
|
||||
onClick={onManualAdd}
|
||||
title={
|
||||
isReadOnly
|
||||
? "Snapshot открыт только для просмотра: ручные ключи добавляются в текущий контекст"
|
||||
: "Ручное добавление ключа"
|
||||
}
|
||||
title="Ручное добавление ключа"
|
||||
type="button"
|
||||
>
|
||||
{isManualAdding ? <Loader2 className="spin" size={15} /> : <Plus size={15} />}
|
||||
|
|
@ -4551,9 +4532,9 @@ function SeoAnchorReviewWorkspace({
|
|||
<button
|
||||
aria-label="Удалить якорь"
|
||||
className="tiny-action icon-only anchor-action-button"
|
||||
disabled={isReadOnly || isBulkApproving}
|
||||
disabled={isBulkApproving}
|
||||
onClick={() => onDeleteRequest(anchor)}
|
||||
title={isReadOnly ? "Snapshot открыт только для просмотра" : "Удалить"}
|
||||
title="Удалить"
|
||||
type="button"
|
||||
>
|
||||
<X size={14} />
|
||||
|
|
@ -4561,7 +4542,7 @@ function SeoAnchorReviewWorkspace({
|
|||
<button
|
||||
aria-label="Скрыть якорь"
|
||||
className={`tiny-action icon-only anchor-action-button${anchor.decision.status === "disabled" ? " active" : ""}`}
|
||||
disabled={isReadOnly || isBulkApproving || isDisableBusy || anchor.decision.status === "disabled"}
|
||||
disabled={isBulkApproving || isDisableBusy || anchor.decision.status === "disabled"}
|
||||
onClick={() =>
|
||||
onDecision(anchor, {
|
||||
anchorId: anchor.id,
|
||||
|
|
@ -4569,7 +4550,7 @@ function SeoAnchorReviewWorkspace({
|
|||
status: "disabled"
|
||||
})
|
||||
}
|
||||
title={isReadOnly ? "Snapshot открыт только для просмотра" : "Скрыть"}
|
||||
title="Скрыть"
|
||||
type="button"
|
||||
>
|
||||
{isDisableBusy ? <Loader2 className="spin" size={14} /> : <EyeOff size={14} />}
|
||||
|
|
@ -4577,7 +4558,7 @@ function SeoAnchorReviewWorkspace({
|
|||
<button
|
||||
aria-label="Оставить якорь на разборе"
|
||||
className={`tiny-action icon-only anchor-action-button${anchor.decision.status === "pending" ? " active" : ""}`}
|
||||
disabled={isReadOnly || isBulkApproving || isPendingBusy || anchor.decision.status === "pending"}
|
||||
disabled={isBulkApproving || isPendingBusy || anchor.decision.status === "pending"}
|
||||
onClick={() =>
|
||||
onDecision(anchor, {
|
||||
anchorId: anchor.id,
|
||||
|
|
@ -4585,7 +4566,7 @@ function SeoAnchorReviewWorkspace({
|
|||
status: "pending"
|
||||
})
|
||||
}
|
||||
title={isReadOnly ? "Snapshot открыт только для просмотра" : "На разбор"}
|
||||
title="На разбор"
|
||||
type="button"
|
||||
>
|
||||
{isPendingBusy ? <Loader2 className="spin" size={14} /> : <RefreshCw size={14} />}
|
||||
|
|
@ -4593,7 +4574,7 @@ function SeoAnchorReviewWorkspace({
|
|||
<button
|
||||
aria-label="Добавить якорь в очередь Wordstat"
|
||||
className={`tiny-action icon-only anchor-action-button${anchor.decision.status === "approved" ? " active" : ""}`}
|
||||
disabled={isReadOnly || isBulkApproving || isApproveBusy || anchor.decision.status === "approved"}
|
||||
disabled={isBulkApproving || isApproveBusy || anchor.decision.status === "approved"}
|
||||
onClick={() =>
|
||||
onDecision(anchor, {
|
||||
anchorId: anchor.id,
|
||||
|
|
@ -4603,7 +4584,7 @@ function SeoAnchorReviewWorkspace({
|
|||
status: "approved"
|
||||
})
|
||||
}
|
||||
title={isReadOnly ? "Snapshot открыт только для просмотра" : "В очередь Wordstat"}
|
||||
title="В очередь Wordstat"
|
||||
type="button"
|
||||
>
|
||||
{isApproveBusy ? <Loader2 className="spin" size={14} /> : <CheckCircle2 size={14} />}
|
||||
|
|
@ -4630,7 +4611,7 @@ function SeoAnchorReviewWorkspace({
|
|||
<NdcGlassSelect<DemandCollectionProfile>
|
||||
ariaLabel="Выбрать профиль сбора спроса"
|
||||
className="anchor-demand-profile-select"
|
||||
disabled={isReadOnly || isBulkApproving || isManualAdding || isSavingDraft || anchorReview.state === "not_ready"}
|
||||
disabled={isBulkApproving || isManualAdding || isSavingDraft || anchorReview.state === "not_ready"}
|
||||
menuClassName="anchor-demand-profile-menu"
|
||||
onChange={onDemandProfileChange}
|
||||
options={demandCollectionProfileOptions}
|
||||
|
|
@ -5706,8 +5687,8 @@ function getPipelineStageAccess(
|
|||
const marketReady = Boolean(input.marketEnrichment && input.marketEnrichment.state !== "not_ready");
|
||||
const keywordReady = Boolean(input.keywordMap && input.keywordMap.readiness.mappedItemCount > 0);
|
||||
const strategyReady = Boolean(input.seoStrategy && input.seoStrategy.state !== "blocked");
|
||||
const synthesisReady = strategyReady && input.strategySynthesis?.state === "ready";
|
||||
const qualityReady = synthesisReady && input.strategyQualityReview?.state === "ready";
|
||||
const synthesisReady = input.strategySynthesis?.state === "ready";
|
||||
const qualityReady = input.strategyQualityReview?.state === "ready";
|
||||
|
||||
if (stageIndex === 0) {
|
||||
return { reason: "Импортируй сайт или выбери уже созданный проект.", status: "open", unlocked: true };
|
||||
|
|
@ -5784,18 +5765,6 @@ function getPipelineStageAccess(
|
|||
};
|
||||
}
|
||||
|
||||
function getPreferredProjectStageIndex(input: Parameters<typeof getPipelineStageAccess>[1]) {
|
||||
const stageAccess = pipeline.map((_, stageIndex) => getPipelineStageAccess(stageIndex, input));
|
||||
|
||||
if (stageAccess[7]?.unlocked) return 7;
|
||||
if (stageAccess[6]?.unlocked) return 6;
|
||||
if (stageAccess[5]?.unlocked) return 5;
|
||||
if (stageAccess[4]?.unlocked) return 4;
|
||||
if (stageAccess[3]?.unlocked) return 3;
|
||||
if (stageAccess[2]?.unlocked) return 2;
|
||||
return 1;
|
||||
}
|
||||
|
||||
function getAuditIssueCounts(issues: AuditIssue[]) {
|
||||
return issues.reduce(
|
||||
(counts, issue) => {
|
||||
|
|
@ -10005,19 +9974,19 @@ export function App() {
|
|||
setPatchArtifacts(Object.fromEntries(patchEntries));
|
||||
}
|
||||
|
||||
async function loadModelProviderStatusForProject(projectId: string) {
|
||||
try {
|
||||
const result = await fetchModelProviderStatus(projectId);
|
||||
setModelProviderStatuses((currentStatuses) => ({
|
||||
...currentStatuses,
|
||||
[projectId]: result.modelProvider
|
||||
}));
|
||||
} catch {
|
||||
setModelProviderStatuses((currentStatuses) => ({
|
||||
...currentStatuses,
|
||||
[projectId]: null
|
||||
}));
|
||||
}
|
||||
async function loadModelProviderStatuses(projectList: ProjectSummary[]) {
|
||||
const statusEntries = await Promise.all(
|
||||
projectList.map(async (project) => {
|
||||
try {
|
||||
const result = await fetchModelProviderStatus(project.id);
|
||||
return [project.id, result.modelProvider] as const;
|
||||
} catch {
|
||||
return [project.id, null] as const;
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
setModelProviderStatuses(Object.fromEntries(statusEntries));
|
||||
}
|
||||
|
||||
async function loadExternalServices() {
|
||||
|
|
@ -10243,9 +10212,7 @@ export function App() {
|
|||
try {
|
||||
const result = await fetchProjects();
|
||||
setProjects(result.projects);
|
||||
setProjectsLoading(false);
|
||||
|
||||
void Promise.all([
|
||||
await Promise.all([
|
||||
loadScanSummaries(result.projects),
|
||||
loadSemanticAnalyses(result.projects),
|
||||
loadContextReviews(result.projects),
|
||||
|
|
@ -10260,15 +10227,15 @@ export function App() {
|
|||
loadSeoStrategies(result.projects),
|
||||
loadStrategySyntheses(result.projects),
|
||||
loadStrategyQualityReviews(result.projects),
|
||||
loadRewritePlans(result.projects),
|
||||
loadMaterializationContracts(result.projects),
|
||||
loadRewriteDiffContracts(result.projects),
|
||||
loadPatchArtifacts(result.projects)
|
||||
]).catch(() => {
|
||||
// Project rows should remain usable even when a secondary contract endpoint is temporarily unavailable.
|
||||
});
|
||||
loadRewritePlans(result.projects),
|
||||
loadMaterializationContracts(result.projects),
|
||||
loadRewriteDiffContracts(result.projects),
|
||||
loadPatchArtifacts(result.projects),
|
||||
loadModelProviderStatuses(result.projects)
|
||||
]);
|
||||
} catch (error: unknown) {
|
||||
setProjectsError(getErrorMessage(error, "Не удалось загрузить проекты."));
|
||||
} finally {
|
||||
setProjectsLoading(false);
|
||||
}
|
||||
}
|
||||
|
|
@ -10784,14 +10751,6 @@ export function App() {
|
|||
}
|
||||
}, [activeProjectId, projects, projectsLoading]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!activeProjectId || !isWorkPanelOpen || Object.prototype.hasOwnProperty.call(modelProviderStatuses, activeProjectId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
void loadModelProviderStatusForProject(activeProjectId);
|
||||
}, [activeProjectId, isWorkPanelOpen, modelProviderStatuses]);
|
||||
|
||||
useEffect(() => {
|
||||
folderInputRef.current?.setAttribute("webkitdirectory", "");
|
||||
folderInputRef.current?.setAttribute("directory", "");
|
||||
|
|
@ -12529,8 +12488,7 @@ export function App() {
|
|||
project: ProjectSummary,
|
||||
source: "expansion_checkpoint" | "manual_save",
|
||||
label?: string,
|
||||
note?: string,
|
||||
options: { activate?: boolean } = {}
|
||||
note?: string
|
||||
) {
|
||||
const keywordMap = keywordMaps[project.id] ?? null;
|
||||
const result = await saveKeywordContextSnapshot(project.id, {
|
||||
|
|
@ -12548,19 +12506,17 @@ export function App() {
|
|||
[project.id]: [result.snapshot, ...projectSnapshots.filter((snapshot) => snapshot.id !== result.snapshot.id)]
|
||||
};
|
||||
});
|
||||
if (options.activate !== false) {
|
||||
setActiveKeywordContextSnapshotIds((currentIds) => ({
|
||||
...currentIds,
|
||||
[project.id]: result.snapshot.id
|
||||
}));
|
||||
}
|
||||
setActiveKeywordContextSnapshotIds((currentIds) => ({
|
||||
...currentIds,
|
||||
[project.id]: result.snapshot.id
|
||||
}));
|
||||
|
||||
return result.snapshot;
|
||||
}
|
||||
|
||||
async function saveKeywordCurationLayoutToActiveSnapshot(project: ProjectSummary) {
|
||||
const projectSnapshots = keywordContextSnapshots[project.id] ?? [];
|
||||
const activeSnapshotId = activeKeywordContextSnapshotIds[project.id] ?? "current";
|
||||
const activeSnapshotId = activeKeywordContextSnapshotIds[project.id] ?? projectSnapshots[0]?.id ?? "current";
|
||||
const activeSnapshot = projectSnapshots.find((snapshot) => snapshot.id === activeSnapshotId) ?? null;
|
||||
|
||||
if (!activeSnapshot || activeSnapshotId === "current") {
|
||||
|
|
@ -12766,8 +12722,7 @@ export function App() {
|
|||
project,
|
||||
"expansion_checkpoint",
|
||||
`Расширение ключей · ${new Date().toLocaleString("ru-RU")}`,
|
||||
"Checkpoint перед добором дополнительных вариантов: ручная канбан-раскладка не сбрасывается.",
|
||||
{ activate: false }
|
||||
"Checkpoint перед добором дополнительных вариантов: ручная канбан-раскладка не сбрасывается."
|
||||
);
|
||||
|
||||
if (keywordMap && (curation.roleOverrides.length > 0 || curation.suppressedItemIds.length > 0)) {
|
||||
|
|
@ -14330,13 +14285,9 @@ export function App() {
|
|||
const activeProject = activeProjectId ? (projects.find((project) => project.id === activeProjectId) ?? null) : null;
|
||||
const activeProjectScanSummary = activeProject ? scanSummaries[activeProject.id] : null;
|
||||
const activeKeywordContextSnapshotList = activeProject ? (keywordContextSnapshots[activeProject.id] ?? []) : [];
|
||||
const requestedKeywordContextSnapshotId = activeProject
|
||||
? (activeKeywordContextSnapshotIds[activeProject.id] ?? "current")
|
||||
: "current";
|
||||
const activeKeywordContextSnapshotId =
|
||||
requestedKeywordContextSnapshotId !== "current" &&
|
||||
activeKeywordContextSnapshotList.some((snapshot) => snapshot.id === requestedKeywordContextSnapshotId)
|
||||
? requestedKeywordContextSnapshotId
|
||||
activeProject && activeKeywordContextSnapshotList.length > 0
|
||||
? (activeKeywordContextSnapshotIds[activeProject.id] ?? activeKeywordContextSnapshotList[0]?.id ?? "current")
|
||||
: "current";
|
||||
const activeKeywordContextSnapshotKey =
|
||||
activeProject && activeKeywordContextSnapshotId !== "current"
|
||||
|
|
@ -14503,14 +14454,6 @@ export function App() {
|
|||
const activeSeoStrategy = activeProject ? (seoStrategies[activeProject.id] ?? null) : null;
|
||||
const activeStrategySynthesis = activeProject ? (strategySyntheses[activeProject.id] ?? null) : null;
|
||||
const activeStrategyQualityReview = activeProject ? (strategyQualityReviews[activeProject.id] ?? null) : null;
|
||||
const activeLiveSemanticAnalysis = activeProject ? (semanticAnalyses[activeProject.id] ?? null) : null;
|
||||
const activeLiveContextReviewRaw = activeProject ? (contextReviews[activeProject.id] ?? null) : null;
|
||||
const activeLiveContextReview =
|
||||
activeLiveSemanticAnalysis && activeLiveContextReviewRaw?.semanticRunId === activeLiveSemanticAnalysis.runId
|
||||
? activeLiveContextReviewRaw
|
||||
: null;
|
||||
const activeLiveMarketEnrichment = activeProject ? (marketEnrichments[activeProject.id] ?? null) : null;
|
||||
const activeLiveKeywordMap = activeProject ? (keywordMaps[activeProject.id] ?? null) : null;
|
||||
const activeProjectScanScopeCounts = activeProjectScanSummary ? getScanScopeCounts(activeProjectScanSummary) : null;
|
||||
const activeProjectSelectedWorkspaceScopeItems = activeProjectScanSummary
|
||||
? getSelectedWorkspaceScopeItems(activeProjectScanSummary)
|
||||
|
|
@ -14592,12 +14535,12 @@ export function App() {
|
|||
const visibleProjects = activeStageIndex === 0 ? projects : activeProject ? [activeProject] : [];
|
||||
const stageAccessList = pipeline.map((_, stageIndex) =>
|
||||
getPipelineStageAccess(stageIndex, {
|
||||
contextReview: activeLiveContextReview,
|
||||
contextReview: activeContextReview,
|
||||
hasProject: Boolean(activeProject),
|
||||
keywordMap: activeLiveKeywordMap,
|
||||
marketEnrichment: activeLiveMarketEnrichment,
|
||||
keywordMap: activeKeywordMap,
|
||||
marketEnrichment: activeMarketEnrichment,
|
||||
scan: activeProjectScanSummary,
|
||||
semanticAnalysis: activeLiveSemanticAnalysis,
|
||||
semanticAnalysis: activeSemanticAnalysis,
|
||||
seoStrategy: activeSeoStrategy,
|
||||
strategyQualityReview: activeStrategyQualityReview,
|
||||
strategySynthesis: activeStrategySynthesis
|
||||
|
|
@ -14996,7 +14939,6 @@ export function App() {
|
|||
disabled={isSavingActiveKeywordContextSnapshot}
|
||||
menuClassName="keyword-context-profile-menu"
|
||||
onChange={(snapshotId) => {
|
||||
clearProjectActionFeedback();
|
||||
setActiveKeywordContextSnapshotIds((currentIds) => ({
|
||||
...currentIds,
|
||||
[activeProject.id]: snapshotId
|
||||
|
|
@ -15252,41 +15194,11 @@ export function App() {
|
|||
</div>
|
||||
) : null;
|
||||
|
||||
function clearProjectActionFeedback() {
|
||||
setProjectActionProjectId(null);
|
||||
setProjectActionError(null);
|
||||
setProjectActionMessage(null);
|
||||
}
|
||||
|
||||
function getProjectPipelineAccessInput(projectId: string): Parameters<typeof getPipelineStageAccess>[1] {
|
||||
const semanticAnalysis = semanticAnalyses[projectId] ?? null;
|
||||
const contextReviewRaw = contextReviews[projectId] ?? null;
|
||||
const contextReview =
|
||||
semanticAnalysis && contextReviewRaw?.semanticRunId === semanticAnalysis.runId ? contextReviewRaw : null;
|
||||
|
||||
return {
|
||||
contextReview,
|
||||
hasProject: true,
|
||||
keywordMap: keywordMaps[projectId] ?? null,
|
||||
marketEnrichment: marketEnrichments[projectId] ?? null,
|
||||
scan: scanSummaries[projectId],
|
||||
semanticAnalysis,
|
||||
seoStrategy: seoStrategies[projectId] ?? null,
|
||||
strategyQualityReview: strategyQualityReviews[projectId] ?? null,
|
||||
strategySynthesis: strategySyntheses[projectId] ?? null
|
||||
};
|
||||
}
|
||||
|
||||
function getPreferredStageIndexForProject(projectId: string) {
|
||||
return getPreferredProjectStageIndex(getProjectPipelineAccessInput(projectId));
|
||||
}
|
||||
|
||||
function openProjectScanStage(project: ProjectSummary) {
|
||||
if (editingProjectId === project.id || pendingDeleteProjectId === project.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
clearProjectActionFeedback();
|
||||
setActiveProjectId(project.id);
|
||||
setActiveStageIndex(1);
|
||||
setIsProjectMenuOpen(false);
|
||||
|
|
@ -15300,20 +15212,17 @@ export function App() {
|
|||
}
|
||||
|
||||
function openProjectFromMenu(project: ProjectSummary) {
|
||||
const nextStageIndex =
|
||||
activeProjectId === project.id && isWorkPanelOpen ? activeStageIndex : getPreferredStageIndexForProject(project.id);
|
||||
const nextStageIndex = activeProjectId === project.id ? activeStageIndex : 1;
|
||||
|
||||
clearProjectActionFeedback();
|
||||
setActiveProjectId(project.id);
|
||||
setActiveStageIndex(nextStageIndex);
|
||||
setIsProjectMenuOpen(false);
|
||||
setIsStageRailOpen(true);
|
||||
setIsWorkPanelOpen(true);
|
||||
setIsWorkPanelOpen(false);
|
||||
setIsWorkPanelFullscreen(false);
|
||||
}
|
||||
|
||||
function closeActiveProject() {
|
||||
clearProjectActionFeedback();
|
||||
setActiveProjectId(null);
|
||||
setActiveStageIndex(0);
|
||||
setIsProjectMenuOpen(false);
|
||||
|
|
@ -15343,7 +15252,10 @@ export function App() {
|
|||
return;
|
||||
}
|
||||
|
||||
clearProjectActionFeedback();
|
||||
if (!projectActionProjectId) {
|
||||
setProjectActionError(null);
|
||||
setProjectActionMessage(null);
|
||||
}
|
||||
|
||||
setActiveStageIndex(stageIndex);
|
||||
setIsStageRailOpen(true);
|
||||
|
|
@ -15385,8 +15297,7 @@ export function App() {
|
|||
project,
|
||||
"manual_save",
|
||||
`Автоснимок перед новым анализом · ${new Date().toLocaleString("ru-RU")}`,
|
||||
"Safety snapshot: текущая карта ключей сохранена перед полным пересбором semantic/context/latest состояния проекта.",
|
||||
{ activate: false }
|
||||
"Safety snapshot: текущая карта ключей сохранена перед полным пересбором semantic/context/latest состояния проекта."
|
||||
);
|
||||
safetySnapshotLabel = safetySnapshot.label;
|
||||
}
|
||||
|
|
@ -19701,40 +19612,39 @@ export function App() {
|
|||
|
||||
{semanticAnalysis && marketEnrichment ? (
|
||||
<>
|
||||
{anchorReview ? (
|
||||
<SeoAnchorReviewWorkspace
|
||||
actionKey={anchorReviewActionKey}
|
||||
activeFilter={activeAnchorReviewFilter}
|
||||
anchorReview={anchorReview}
|
||||
contextProfileLabel={projectKeywordContextProfileLabel}
|
||||
contextProfileStatus={projectKeywordContextProfileStatus}
|
||||
draftDecisionCount={anchorReviewDraftCount}
|
||||
demandCollectionProfile={anchorReviewDemandProfile}
|
||||
draftChangeCount={anchorReviewDraftChangeCount}
|
||||
isCollapsed={isAnchorStageCollapsed}
|
||||
isDemandProfileDirty={isAnchorReviewDemandProfileDirty}
|
||||
isBulkApproving={isApprovingAnchorReview}
|
||||
isReadOnly={projectKeywordContextProfileStatus !== "current"}
|
||||
isSavingDraft={savingAnchorReviewDraftProjectId === project.id}
|
||||
isManualAdding={anchorReviewActionKey === `${project.id}:manual-anchor`}
|
||||
onDecision={(_anchor, decision) => void handleAnchorReviewDecision(project, decision)}
|
||||
onDeleteRequest={(anchor) => openAnchorDeleteModal(project, anchor)}
|
||||
onDemandProfileChange={(profile) =>
|
||||
handleAnchorReviewDemandProfileChange(project, profile)
|
||||
}
|
||||
onFilterChange={(filter) =>
|
||||
setAnchorReviewFilters((currentFilters) => ({
|
||||
...currentFilters,
|
||||
[project.id]: filter
|
||||
}))
|
||||
}
|
||||
onToggleCollapse={() => toggleKeywordStageCollapse(project.id, "anchors")}
|
||||
onManualAdd={() => openManualAnchorModal(project)}
|
||||
onSaveDraft={() => void handleSaveAnchorReviewDraft(project)}
|
||||
stageProjectId={project.id}
|
||||
sectionId={`keyword-anchor-review:${project.id}`}
|
||||
/>
|
||||
) : null}
|
||||
{anchorReview ? (
|
||||
<SeoAnchorReviewWorkspace
|
||||
actionKey={anchorReviewActionKey}
|
||||
activeFilter={activeAnchorReviewFilter}
|
||||
anchorReview={anchorReview}
|
||||
contextProfileLabel={projectKeywordContextProfileLabel}
|
||||
contextProfileStatus={projectKeywordContextProfileStatus}
|
||||
draftDecisionCount={anchorReviewDraftCount}
|
||||
demandCollectionProfile={anchorReviewDemandProfile}
|
||||
draftChangeCount={anchorReviewDraftChangeCount}
|
||||
isCollapsed={isAnchorStageCollapsed}
|
||||
isDemandProfileDirty={isAnchorReviewDemandProfileDirty}
|
||||
isBulkApproving={isApprovingAnchorReview}
|
||||
isSavingDraft={savingAnchorReviewDraftProjectId === project.id}
|
||||
isManualAdding={anchorReviewActionKey === `${project.id}:manual-anchor`}
|
||||
onDecision={(_anchor, decision) => void handleAnchorReviewDecision(project, decision)}
|
||||
onDeleteRequest={(anchor) => openAnchorDeleteModal(project, anchor)}
|
||||
onDemandProfileChange={(profile) =>
|
||||
handleAnchorReviewDemandProfileChange(project, profile)
|
||||
}
|
||||
onFilterChange={(filter) =>
|
||||
setAnchorReviewFilters((currentFilters) => ({
|
||||
...currentFilters,
|
||||
[project.id]: filter
|
||||
}))
|
||||
}
|
||||
onToggleCollapse={() => toggleKeywordStageCollapse(project.id, "anchors")}
|
||||
onManualAdd={() => openManualAnchorModal(project)}
|
||||
onSaveDraft={() => void handleSaveAnchorReviewDraft(project)}
|
||||
stageProjectId={project.id}
|
||||
sectionId={`keyword-anchor-review:${project.id}`}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{showKeywordDemandAnalysis ? (
|
||||
<section
|
||||
|
|
|
|||
Loading…
Reference in New Issue