@@ -66,6 +66,7 @@ import {
deleteProject ,
deleteProject ,
downloadSemanticAnalysisExport ,
downloadSemanticAnalysisExport ,
fetchExternalServiceSettings ,
fetchExternalServiceSettings ,
fetchKeywordContextSnapshot ,
fetchKeywordContextSnapshots ,
fetchKeywordContextSnapshots ,
fetchLatestKeywordAnalysisWorkflow ,
fetchLatestKeywordAnalysisWorkflow ,
fetchModelProviderStatus ,
fetchModelProviderStatus ,
@@ -128,6 +129,7 @@ import {
type HealthResponse ,
type HealthResponse ,
type KeywordAnalysisWorkflowContract ,
type KeywordAnalysisWorkflowContract ,
type KeywordCleaningContract ,
type KeywordCleaningContract ,
type KeywordContextSnapshotDetail ,
type KeywordContextSnapshotSummary ,
type KeywordContextSnapshotSummary ,
type KeywordMapContract ,
type KeywordMapContract ,
type KeywordMapRole ,
type KeywordMapRole ,
@@ -331,7 +333,15 @@ const dateFormatter = new Intl.DateTimeFormat("ru-RU", {
const numberFormatter = new Intl . NumberFormat ( "ru-RU" ) ;
const numberFormatter = new Intl . NumberFormat ( "ru-RU" ) ;
function getErrorMessage ( error : unknown , fallback : string ) {
function getErrorMessage ( error : unknown , fallback : string ) {
return error instanceof Error ? error.message : fallback ;
if ( ! ( error instanceof Error ) ) {
return fallback ;
}
if ( error . message === "Failed to fetch" ) {
return ` ${ fallback } : backend API недоступен или вкладка потеряла соединение с http://localhost:4100. Обнови страницу после запуска сервера. ` ;
}
return error . message ;
}
}
function formatDate ( value : string ) {
function formatDate ( value : string ) {
@@ -6345,6 +6355,28 @@ function getKeywordSnapshotTimestamp(snapshot: KeywordContextSnapshotSummary) {
return Date . parse ( snapshot . keywordMapGeneratedAt ? ? snapshot . createdAt ) ;
return Date . parse ( snapshot . keywordMapGeneratedAt ? ? snapshot . createdAt ) ;
}
}
function getKeywordContextSnapshotDetailKey ( projectId : string , snapshotId : string ) {
return ` ${ projectId } : ${ snapshotId } ` ;
}
function getKeywordContextSnapshotOverrideRecord ( snapshot : KeywordContextSnapshotDetail | null ) {
const roleOverrides = snapshot ? . snapshot . curation ? . roleOverrides ? ? [ ] ;
return Object . fromEntries (
roleOverrides
. map ( ( override ) = > {
const role = normalizeKeywordCurationRole ( override . role ) ;
return keywordCurationColumns . some ( ( column ) = > column . role === role ) ? [ override . itemId , role ] : null ;
} )
. filter ( ( entry ) : entry is [ string , KeywordCurationRole ] = > Boolean ( entry ) )
) ;
}
function getKeywordContextSnapshotSuppressedIds ( snapshot : KeywordContextSnapshotDetail | null ) {
return snapshot ? . snapshot . curation ? . suppressedItemIds ? ? [ ] ;
}
function getKeywordExpansionBaselineSnapshot ( keywordMap : KeywordMapContract , snapshots : KeywordContextSnapshotSummary [ ] ) {
function getKeywordExpansionBaselineSnapshot ( keywordMap : KeywordMapContract , snapshots : KeywordContextSnapshotSummary [ ] ) {
const keywordMapTimestamp = Date . parse ( keywordMap . generatedAt ) ;
const keywordMapTimestamp = Date . parse ( keywordMap . generatedAt ) ;
@@ -9556,6 +9588,10 @@ export function App() {
const [ keywordCurationSuppressedItemIds , setKeywordCurationSuppressedItemIds ] = useState < Record < string , string [ ] > > ( { } ) ;
const [ keywordCurationSuppressedItemIds , setKeywordCurationSuppressedItemIds ] = useState < Record < string , string [ ] > > ( { } ) ;
const [ keywordContextSnapshots , setKeywordContextSnapshots ] = useState < Record < string , KeywordContextSnapshotSummary [ ] > > ( { } ) ;
const [ keywordContextSnapshots , setKeywordContextSnapshots ] = useState < Record < string , KeywordContextSnapshotSummary [ ] > > ( { } ) ;
const [ activeKeywordContextSnapshotIds , setActiveKeywordContextSnapshotIds ] = useState < Record < string , string > > ( { } ) ;
const [ activeKeywordContextSnapshotIds , setActiveKeywordContextSnapshotIds ] = useState < Record < string , string > > ( { } ) ;
const [ keywordContextSnapshotDetails , setKeywordContextSnapshotDetails ] = useState <
Record < string , KeywordContextSnapshotDetail >
> ( { } ) ;
const [ loadingKeywordContextSnapshotKey , setLoadingKeywordContextSnapshotKey ] = useState < string | null > ( null ) ;
const [ savingKeywordContextSnapshotProjectId , setSavingKeywordContextSnapshotProjectId ] = useState < string | null > ( null ) ;
const [ savingKeywordContextSnapshotProjectId , setSavingKeywordContextSnapshotProjectId ] = useState < string | null > ( null ) ;
const [ savingKeywordCurationLayoutProjectId , setSavingKeywordCurationLayoutProjectId ] = useState < string | null > ( null ) ;
const [ savingKeywordCurationLayoutProjectId , setSavingKeywordCurationLayoutProjectId ] = useState < string | null > ( null ) ;
const [ pendingKeywordContextSnapshotDialog , setPendingKeywordContextSnapshotDialog ] =
const [ pendingKeywordContextSnapshotDialog , setPendingKeywordContextSnapshotDialog ] =
@@ -9756,6 +9792,45 @@ export function App() {
setKeywordContextSnapshots ( Object . fromEntries ( snapshotEntries ) ) ;
setKeywordContextSnapshots ( Object . fromEntries ( snapshotEntries ) ) ;
}
}
async function loadKeywordContextSnapshotDetail ( projectId : string , snapshotId : string ) {
if ( snapshotId === "current" ) {
return null ;
}
const detailKey = getKeywordContextSnapshotDetailKey ( projectId , snapshotId ) ;
const cachedDetail = keywordContextSnapshotDetails [ detailKey ] ;
if ( cachedDetail ) {
return cachedDetail ;
}
setLoadingKeywordContextSnapshotKey ( detailKey ) ;
try {
const result = await fetchKeywordContextSnapshot ( projectId , snapshotId ) ;
setKeywordContextSnapshotDetails ( ( currentDetails ) = > ( {
. . . currentDetails ,
[ detailKey ] : result
} ) ) ;
setKeywordContextSnapshots ( ( currentSnapshots ) = > ( {
. . . currentSnapshots ,
[ projectId ] : ( currentSnapshots [ projectId ] ? ? [ ] ) . map ( ( snapshot ) = >
snapshot . id === result . summary . id ? result.summary : snapshot
)
} ) ) ;
return result ;
} catch ( error : unknown ) {
setProjectActionProjectId ( projectId ) ;
setProjectActionError ( getErrorMessage ( error , "Не удалось загрузить сохранённый контекст ключей." ) ) ;
setProjectActionMessage ( null ) ;
return null ;
} finally {
setLoadingKeywordContextSnapshotKey ( ( currentKey ) = > ( currentKey === detailKey ? null : currentKey ) ) ;
}
}
async function loadSeoStrategies ( projectList : ProjectSummary [ ] ) {
async function loadSeoStrategies ( projectList : ProjectSummary [ ] ) {
const strategyEntries = await Promise . all (
const strategyEntries = await Promise . all (
projectList . map ( async ( project ) = > {
projectList . map ( async ( project ) = > {
@@ -10694,7 +10769,6 @@ export function App() {
const timeoutId = window . setTimeout ( ( ) = > {
const timeoutId = window . setTimeout ( ( ) = > {
setProjectActionMessage ( null ) ;
setProjectActionMessage ( null ) ;
setProjectActionProjectId ( null ) ;
} , NOTICE_AUTO_HIDE_MS ) ;
} , NOTICE_AUTO_HIDE_MS ) ;
return ( ) = > {
return ( ) = > {
@@ -12379,15 +12453,22 @@ export function App() {
setPendingKeywordSuppress ( null ) ;
setPendingKeywordSuppress ( null ) ;
}
}
function getKeywordContextSnapshotCuration ( projectId : string , keywordMap : KeywordMapContract | null ) {
function getKeywordContextSnapshotCuration (
const suppressedItemIds = keywordCurationSuppressedItemIds [ projectId ] ? ? [ ] ;
projectId : string ,
keywordMap : KeywordMapContract | null ,
curationState ? : {
roleOverrides? : Record < string , KeywordCurationRole > ;
suppressedItemIds? : string [ ] ;
}
) {
const suppressedItemIds = curationState ? . suppressedItemIds ? ? keywordCurationSuppressedItemIds [ projectId ] ? ? [ ] ;
const suppressedItemIdSet = new Set ( suppressedItemIds ) ;
const suppressedItemIdSet = new Set ( suppressedItemIds ) ;
return {
return {
roleOverrides : keywordMap
roleOverrides : keywordMap
? getKeywordCurationRoleOverrides (
? getKeywordCurationRoleOverrides (
keywordMap ,
keywordMap ,
keywordCurationRoleOverrides [ projectId ] ? ? { } ,
curationState ? . roleOverrides ? ? keywordCurationRoleOverrides [ projectId ] ? ? { } ,
suppressedItemIdSet
suppressedItemIdSet
)
)
: [ ] ,
: [ ] ,
@@ -12442,32 +12523,21 @@ export function App() {
setProjectActionProjectId ( project . id ) ;
setProjectActionProjectId ( project . id ) ;
try {
try {
const keywordMap = keywordMaps [ project . id ] ? ? null ;
const detailKey = getKeywordContextSnapshotDetailKey ( project . id , activeSnapshot . id ) ;
const curation = getKeywordContextSnapshotCuration ( project . id , keywordMap ) ;
const snapshotDetail =
keywordContextSnapshotDetails [ detailKey ] ? ? ( await loadKeywordContextSnapshotDetail ( project . id , activeSnapshot . id ) ) ;
if ( keywordMap && ( curation . roleOverrides . length > 0 || curation . suppressedItemIds . length > 0 ) ) {
const keywordMap = snapshotDetail ? . snapshot . keywordMap ? ? keywordMaps [ project . id ] ? ? null ;
const changedItemIds = Array . from (
const snapshotRoleOverrides = getKeywordContextSnapshotOverrideRecord ( snapshotDetail ) ;
new Set ( [ . . . curation . suppressedItemIds , . . . curation . roleOverrides . map ( ( override ) = > override . itemId ) ] )
const liveRoleOverrides = keywordCurationRoleOverrides [ project . id ] ? ? { } ;
) ;
const snapshotSuppressedIds = getKeywordContextSnapshotSuppressedIds ( snapshotDetail ) ;
const curationResult = await approveKeywordMapDecisions ( project . id , {
const liveSuppressedIds = keywordCurationSuppressedItemIds [ project . id ] ? ? [ ] ;
itemIds : changedItemIds ,
const curation = getKeywordContextSnapshotCuration ( project . id , keywordMap , {
roleOverrides : curation.roleOverrides ,
roleOverrides : {
suppressedItemIds : curation.suppressedItemIds
. . . snapshotRoleOverrides ,
} ) ;
. . . liveRoleOverrides
} ,
setKeywordMaps ( ( currentKeywordMaps ) = > ( {
suppressedItemIds : Array.from ( new Set ( [ . . . snapshotSuppressedIds , . . . liveSuppressedIds ] ) )
. . . currentKeywordMaps ,
} ) ;
[ project . id ] : curationResult . keywordMap
} ) ) ;
setKeywordCurationRoleOverrides ( ( currentOverrides ) = > ( {
. . . currentOverrides ,
[ project . id ] : { }
} ) ) ;
setKeywordCurationSuppressedItemIds ( ( currentSuppressed ) = > ( {
. . . currentSuppressed ,
[ project . id ] : [ ]
} ) ) ;
}
const result = await updateKeywordContextSnapshotCuration (
const result = await updateKeywordContextSnapshotCuration (
project . id ,
project . id ,
@@ -12481,6 +12551,25 @@ export function App() {
snapshot . id === result . snapshot . id ? result.snapshot : snapshot
snapshot . id === result . snapshot . id ? result.snapshot : snapshot
)
)
} ) ) ;
} ) ) ;
setKeywordContextSnapshotDetails ( ( currentDetails ) = > {
const currentDetail = currentDetails [ detailKey ] ;
if ( ! currentDetail ) {
return currentDetails ;
}
return {
. . . currentDetails ,
[ detailKey ] : {
. . . currentDetail ,
snapshot : {
. . . currentDetail . snapshot ,
curation
} ,
summary : result.snapshot
}
} ;
} ) ;
setActiveKeywordContextSnapshotIds ( ( currentIds ) = > ( {
setActiveKeywordContextSnapshotIds ( ( currentIds ) = > ( {
. . . currentIds ,
. . . currentIds ,
[ project . id ] : result . snapshot . id
[ project . id ] : result . snapshot . id
@@ -12583,6 +12672,13 @@ export function App() {
setProjectActionMessage ( ` Контекст переименован: ${ result . snapshot . label } . ` ) ;
setProjectActionMessage ( ` Контекст переименован: ${ result . snapshot . label } . ` ) ;
} else {
} else {
await deleteKeywordContextSnapshot ( project . id , dialog . snapshotId ) ;
await deleteKeywordContextSnapshot ( project . id , dialog . snapshotId ) ;
setKeywordContextSnapshotDetails ( ( currentDetails ) = >
Object . fromEntries (
Object . entries ( currentDetails ) . filter (
( [ detailKey ] ) = > detailKey !== getKeywordContextSnapshotDetailKey ( project . id , dialog . snapshotId )
)
)
) ;
setKeywordContextSnapshots ( ( currentSnapshots ) = > {
setKeywordContextSnapshots ( ( currentSnapshots ) = > {
const nextSnapshots = ( currentSnapshots [ project . id ] ? ? [ ] ) . filter ( ( snapshot ) = > snapshot . id !== dialog . snapshotId ) ;
const nextSnapshots = ( currentSnapshots [ project . id ] ? ? [ ] ) . filter ( ( snapshot ) = > snapshot . id !== dialog . snapshotId ) ;
@@ -14180,16 +14276,89 @@ export function App() {
const activeProject = activeProjectId ? ( projects . find ( ( project ) = > project . id === activeProjectId ) ? ? null ) : null ;
const activeProject = activeProjectId ? ( projects . find ( ( project ) = > project . id === activeProjectId ) ? ? null ) : null ;
const activeProjectScanSummary = activeProject ? scanSummaries [ activeProject . id ] : null ;
const activeProjectScanSummary = activeProject ? scanSummaries [ activeProject . id ] : null ;
const activeSemanticAnalysis = activeProject ? ( semanticAnalyses [ activeProject . id ] ? ? null ) : null ;
const activeKeywordContextSnapshotList = activeProject ? ( keywordContextSnapshots [ activeProject . id ] ? ? [ ] ) : [ ] ;
const activeKeywordContextSnapshotId =
activeProject && activeKeywordContextSnapshotList . length > 0
? ( activeKeywordContextSnapshotIds [ activeProject . id ] ? ? activeKeywordContextSnapshotList [ 0 ] ? . id ? ? "current" )
: "current" ;
const activeKeywordContextSnapshotKey =
activeProject && activeKeywordContextSnapshotId !== "current"
? getKeywordContextSnapshotDetailKey ( activeProject . id , activeKeywordContextSnapshotId )
: null ;
const activeKeywordContextSnapshotDetail = activeKeywordContextSnapshotKey
? ( keywordContextSnapshotDetails [ activeKeywordContextSnapshotKey ] ? ? null )
: null ;
const isActiveKeywordContextSnapshotLoading = Boolean (
activeKeywordContextSnapshotKey && loadingKeywordContextSnapshotKey === activeKeywordContextSnapshotKey
) ;
const activeSemanticAnalysis =
activeKeywordContextSnapshotDetail ? . snapshot . semanticAnalysis ? ? ( activeProject ? ( semanticAnalyses [ activeProject . id ] ? ? null ) : null ) ;
const activeContextReviewRaw = activeProject ? ( contextReviews [ activeProject . id ] ? ? null ) : null ;
const activeContextReviewRaw = activeProject ? ( contextReviews [ activeProject . id ] ? ? null ) : null ;
const activeContextReview =
const activeContextReview =
activeSemanticAnalysis && activeContextReviewRaw ? . semanticRunId === activeSemanticAnalysis . runId
activeSemanticAnalysis && activeContextReviewRaw ? . semanticRunId === activeSemanticAnalysis . runId
? activeContextReviewRaw
? activeContextReviewRaw
: null ;
: null ;
const activeMarketEnrichment = activeProject ? ( marketEnrichments [ activeProject . id ] ? ? null ) : null ;
const activeMarketEnrichment =
activeKeywordContextSnapshotDetail ? . snapshot . marketEnrichment ? ?
( activeProject ? ( marketEnrichments [ activeProject . id ] ? ? null ) : null ) ;
const activeKeywordAnalysisWorkflow = activeProject ? ( keywordAnalysisWorkflows [ activeProject . id ] ? ? null ) : null ;
const activeKeywordAnalysisWorkflow = activeProject ? ( keywordAnalysisWorkflows [ activeProject . id ] ? ? null ) : null ;
const activeKeywordMap =
activeKeywordContextSnapshotDetail ? . snapshot . keywordMap ? ? ( activeProject ? ( keywordMaps [ activeProject . id ] ? ? null ) : null ) ;
const activeQuotaProjectId = activeProject ? . id ? ? null ;
const activeQuotaProjectId = activeProject ? . id ? ? null ;
useEffect ( ( ) = > {
if ( activeStageIndex !== 4 || ! activeQuotaProjectId ) {
return ;
}
let cancelled = false ;
void fetchKeywordContextSnapshots ( activeQuotaProjectId )
. then ( ( result ) = > {
if ( cancelled ) {
return ;
}
setKeywordContextSnapshots ( ( currentSnapshots ) = > ( {
. . . currentSnapshots ,
[ activeQuotaProjectId ] : result . snapshots
} ) ) ;
} )
. catch ( ( error : unknown ) = > {
if ( cancelled ) {
return ;
}
setProjectActionProjectId ( activeQuotaProjectId ) ;
setProjectActionError ( getErrorMessage ( error , "Не удалось обновить snapshot-профили ключей." ) ) ;
setProjectActionMessage ( null ) ;
} ) ;
return ( ) = > {
cancelled = true ;
} ;
} , [ activeQuotaProjectId , activeStageIndex ] ) ;
useEffect ( ( ) = > {
if ( ! activeProject || activeStageIndex !== 4 || activeKeywordContextSnapshotId === "current" ) {
return ;
}
const detailKey = getKeywordContextSnapshotDetailKey ( activeProject . id , activeKeywordContextSnapshotId ) ;
if ( keywordContextSnapshotDetails [ detailKey ] || loadingKeywordContextSnapshotKey === detailKey ) {
return ;
}
void loadKeywordContextSnapshotDetail ( activeProject . id , activeKeywordContextSnapshotId ) ;
} , [
activeKeywordContextSnapshotId ,
activeProject ? . id ,
activeStageIndex ,
keywordContextSnapshotDetails ,
loadingKeywordContextSnapshotKey
] ) ;
useEffect ( ( ) = > {
useEffect ( ( ) = > {
if ( activeStageIndex !== 4 || ! activeQuotaProjectId ) {
if ( activeStageIndex !== 4 || ! activeQuotaProjectId ) {
return ;
return ;
@@ -14267,7 +14436,6 @@ export function App() {
return ( ) = > window . clearInterval ( intervalId ) ;
return ( ) = > window . clearInterval ( intervalId ) ;
} , [ activeKeywordAnalysisWorkflow ? . runId , activeKeywordAnalysisWorkflow ? . status , activeQuotaProjectId , activeStageIndex ] ) ;
} , [ activeKeywordAnalysisWorkflow ? . runId , activeKeywordAnalysisWorkflow ? . status , activeQuotaProjectId , activeStageIndex ] ) ;
const activeKeywordMap = activeProject ? ( keywordMaps [ activeProject . id ] ? ? null ) : null ;
const activeSeoStrategy = activeProject ? ( seoStrategies [ activeProject . id ] ? ? null ) : null ;
const activeSeoStrategy = activeProject ? ( seoStrategies [ activeProject . id ] ? ? null ) : null ;
const activeStrategySynthesis = activeProject ? ( strategySyntheses [ activeProject . id ] ? ? null ) : null ;
const activeStrategySynthesis = activeProject ? ( strategySyntheses [ activeProject . id ] ? ? null ) : null ;
const activeStrategyQualityReview = activeProject ? ( strategyQualityReviews [ activeProject . id ] ? ? null ) : null ;
const activeStrategyQualityReview = activeProject ? ( strategyQualityReviews [ activeProject . id ] ? ? null ) : null ;
@@ -14629,11 +14797,16 @@ export function App() {
title : workspaceMode === "dev" ? activeStage . title : "Автоматический SEO-процесс" ,
title : workspaceMode === "dev" ? activeStage . title : "Автоматический SEO-процесс" ,
subtitle : workspaceMode === "dev" ? ` Dev Mode · этап ${ activeStageIndex + 1 } ` : "Auto Mode · целевой слой"
subtitle : workspaceMode === "dev" ? ` Dev Mode · этап ${ activeStageIndex + 1 } ` : "Auto Mode · целевой слой"
} ;
} ;
const activeAnchorReviewRaw = activeMarketEnrichment ? . anchorReview ? ? null ;
const activeAnchorReviewRaw =
activeKeywordContextSnapshotDetail ? . snapshot . anchorReview ? ? activeMarketEnrichment ? . anchorReview ? ? null ;
const activeAnchorReviewDraft = activeProject ? ( anchorReviewDraftDecisions [ activeProject . id ] ? ? { } ) : { } ;
const activeAnchorReviewDraft = activeProject ? ( anchorReviewDraftDecisions [ activeProject . id ] ? ? { } ) : { } ;
const activeAnchorReviewDraftCount = getAnchorReviewDraftDecisionList ( activeAnchorReviewRaw , activeAnchorReviewDraft ) . length ;
const activeAnchorReviewDraftCount = getAnchorReviewDraftDecisionList ( activeAnchorReviewRaw , activeAnchorReviewDraft ) . length ;
const activeAnchorReview = activeAnchorReviewRaw ? applyAnchorReviewDraft ( activeAnchorReviewRaw , activeAnchorReviewDraft ) : null ;
const activeAnchorReview = activeAnchorReviewRaw ? applyAnchorReviewDraft ( activeAnchorReviewRaw , activeAnchorReviewDraft ) : null ;
const activeAnchorReviewReady = Boolean ( activeMarketEnrichment ? . readiness . anchorReviewReady ) ;
const activeAnchorReviewReady = Boolean (
activeMarketEnrichment ? . readiness . anchorReviewReady ||
activeAnchorReviewRaw ? . state === "approved" ||
( activeAnchorReviewRaw ? . readiness . wordstatQueueCount ? ? 0 ) > 0
) ;
const activeKeywordWordstatOutdated = activeMarketEnrichment ? isMarketWordstatOutdated ( activeMarketEnrichment ) : false ;
const activeKeywordWordstatOutdated = activeMarketEnrichment ? isMarketWordstatOutdated ( activeMarketEnrichment ) : false ;
const activeKeywordAnalysisRebuilding = isKeywordAnalysisWorkflowActive ( activeKeywordAnalysisWorkflow ) ;
const activeKeywordAnalysisRebuilding = isKeywordAnalysisWorkflowActive ( activeKeywordAnalysisWorkflow ) ;
const activeKeywordAnalysisReady = Boolean (
const activeKeywordAnalysisReady = Boolean (
@@ -14642,8 +14815,20 @@ export function App() {
? activeKeywordAnalysisWorkflow . status === "completed"
? activeKeywordAnalysisWorkflow . status === "completed"
: activeMarketEnrichment && isMarketAnalysisReady ( activeMarketEnrichment ) )
: activeMarketEnrichment && isMarketAnalysisReady ( activeMarketEnrichment ) )
) ;
) ;
const activeKeywordCurationOverrides = activeProject ? ( keywordCurationRoleOverrides [ activeProject . id ] ? ? { } ) : { } ;
const activeKeywordCurationOverrides = activeProject
const activeKeywordCurationSuppressedIds = activeProject ? ( keywordCurationSuppressedItemIds [ activeProject . id ] ? ? [ ] ) : [ ] ;
? {
. . . getKeywordContextSnapshotOverrideRecord ( activeKeywordContextSnapshotDetail ) ,
. . . ( keywordCurationRoleOverrides [ activeProject . id ] ? ? { } )
}
: { } ;
const activeKeywordCurationSuppressedIds = activeProject
? Array . from (
new Set ( [
. . . getKeywordContextSnapshotSuppressedIds ( activeKeywordContextSnapshotDetail ) ,
. . . ( keywordCurationSuppressedItemIds [ activeProject . id ] ? ? [ ] )
] )
)
: [ ] ;
const activeKeywordCurationSuppressedSet = new Set ( activeKeywordCurationSuppressedIds ) ;
const activeKeywordCurationSuppressedSet = new Set ( activeKeywordCurationSuppressedIds ) ;
const activeKeywordWorkflowStep =
const activeKeywordWorkflowStep =
activeProject && activeStageIndex === 4
activeProject && activeStageIndex === 4
@@ -14699,11 +14884,7 @@ export function App() {
! isPersistingActiveKeywordMap
! isPersistingActiveKeywordMap
) ;
) ;
const isSubmittingActiveKeywordStrategy = isPersistingActiveKeywordMap && persistingKeywordMapMode === "strategy" ;
const isSubmittingActiveKeywordStrategy = isPersistingActiveKeywordMap && persistingKeywordMapMode === "strategy" ;
const activeKeywordContextSnapshotList = activeProject ? ( keywordContextSnapshots [ activeProject . id ] ? ? [ ] ) : [ ] ;
const isActiveKeywordContextSnapshotReplay = activeKeywordContextSnapshotId !== "current" ;
const activeKeywordContextSnapshotId =
activeProject && activeKeywordContextSnapshotList . length > 0
? ( activeKeywordContextSnapshotIds [ activeProject . id ] ? ? activeKeywordContextSnapshotList [ 0 ] ? . id ? ? "current" )
: "current" ;
const activeKeywordContextSnapshotOptions : NdcGlassSelectOption < string > [ ] = [
const activeKeywordContextSnapshotOptions : NdcGlassSelectOption < string > [ ] = [
{ label : "Текущий контекст" , value : "current" } ,
{ label : "Текущий контекст" , value : "current" } ,
. . . activeKeywordContextSnapshotList . map ( ( snapshot ) = > ( {
. . . activeKeywordContextSnapshotList . map ( ( snapshot ) = > ( {
@@ -14738,15 +14919,24 @@ export function App() {
className = "keyword-context-profile-select"
className = "keyword-context-profile-select"
disabled = { isSavingActiveKeywordContextSnapshot }
disabled = { isSavingActiveKeywordContextSnapshot }
menuClassName = "keyword-context-profile-menu"
menuClassName = "keyword-context-profile-menu"
onChange = { ( snapshotId ) = >
onChange = { ( snapshotId ) = > {
setActiveKeywordContextSnapshotIds ( ( currentIds ) = > ( {
setActiveKeywordContextSnapshotIds ( ( currentIds ) = > ( {
. . . currentIds ,
. . . currentIds ,
[ activeProject . id ] : snapshotId
[ activeProject . id ] : snapshotId
} ) )
} ) ) ;
}
if ( snapshotId !== "current" ) {
void loadKeywordContextSnapshotDetail ( activeProject . id , snapshotId ) ;
}
} }
options = { activeKeywordContextSnapshotOptions }
options = { activeKeywordContextSnapshotOptions }
value = { activeKeywordContextSnapshotId }
value = { activeKeywordContextSnapshotId }
/ >
/ >
{ isActiveKeywordContextSnapshotReplay ? (
< span className = "keyword-context-profile-status" >
{ isActiveKeywordContextSnapshotLoading ? "загружаю снимок" : "снимок" }
< / span >
) : null }
< button
< button
aria - label = "Сохранить как новый профиль контекста ключей"
aria - label = "Сохранить как новый профиль контекста ключей"
className = { isSavingActiveKeywordContextSnapshot ? "saving" : undefined }
className = { isSavingActiveKeywordContextSnapshot ? "saving" : undefined }
@@ -14824,7 +15014,7 @@ export function App() {
{ activeKeywordActionStep === "curation" && activeKeywordMap ? (
{ activeKeywordActionStep === "curation" && activeKeywordMap ? (
< button
< button
className = "active"
className = "active"
disabled = { ! canSubmitActiveKeywordStrategy }
disabled = { ! canSubmitActiveKeywordStrategy || isActiveKeywordContextSnapshotReplay }
onClick = { ( ) = >
onClick = { ( ) = >
void handleApproveKeywordMapDecisions ( activeProject , {
void handleApproveKeywordMapDecisions ( activeProject , {
jumpToStrategy : true ,
jumpToStrategy : true ,
@@ -14837,7 +15027,9 @@ export function App() {
} )
} )
}
}
title = {
title = {
activeKeywordMap . state === "draft"
isActiveKeywordContextSnapshotReplay
? "Сначала переключись на текущий контекст: сохранённый snapshot открыт только для просмотра и раскладки."
: activeKeywordMap . state === "draft"
? "Сформировать стратегию по распределённым ключам"
? "Сформировать стратегию по распределённым ключам"
: "Карта ключей заблокирована backend-гейтами"
: "Карта ключей заблокирована backend-гейтами"
}
}
@@ -14848,7 +15040,16 @@ export function App() {
< / button >
< / button >
) : activeKeywordActionStep === "analysis" && activeAnchorReviewReady ? (
) : activeKeywordActionStep === "analysis" && activeAnchorReviewReady ? (
activeKeywordAnalysisReady ? (
activeKeywordAnalysisReady ? (
< button onClick = { ( ) = > handleKeywordReconfigure ( activeProject . id ) } type = "button" >
< button
disabled = { isActiveKeywordContextSnapshotReplay }
onClick = { ( ) = > handleKeywordReconfigure ( activeProject . id ) }
title = {
isActiveKeywordContextSnapshotReplay
? "Сначала переключись на текущий контекст: snapshot не запускает новый анализ."
: "Переконфигурировать ключи"
}
type = "button"
>
< RefreshCw size = { 14 } / >
< RefreshCw size = { 14 } / >
П е р е к о н ф и г у р и р о в а т ь к л ю ч и
П е р е к о н ф и г у р и р о в а т ь к л ю ч и
< / button >
< / button >
@@ -14867,12 +15068,15 @@ export function App() {
className = "active"
className = "active"
disabled = {
disabled = {
isApprovingActiveAnchorReview ||
isApprovingActiveAnchorReview ||
isActiveKeywordContextSnapshotReplay ||
activeAnchorReview . state === "not_ready" ||
activeAnchorReview . state === "not_ready" ||
activeAnchorReview . readiness . wordstatQueueCount === 0
activeAnchorReview . readiness . wordstatQueueCount === 0
}
}
onClick = { ( ) = > void handleAnchorReviewApproval ( activeProject ) }
onClick = { ( ) = > void handleAnchorReviewApproval ( activeProject ) }
title = {
title = {
activeAnchorReviewDraftCount > 0
isActiveKeywordContextSnapshotReplay
? "Сначала переключись на текущий контекст: snapshot не запускает новый рыночный анализ."
: activeAnchorReviewDraftCount > 0
? "Автосохранить текущую раскладку и отправить семантический базис на рыночный анализ"
? "Автосохранить текущую раскладку и отправить семантический базис на рыночный анализ"
: "Отправить семантический базис на рыночный анализ"
: "Отправить семантический базис на рыночный анализ"
}
}
@@ -15029,6 +15233,11 @@ export function App() {
return ;
return ;
}
}
if ( ! projectActionProjectId ) {
setProjectActionError ( null ) ;
setProjectActionMessage ( null ) ;
}
setActiveStageIndex ( stageIndex ) ;
setActiveStageIndex ( stageIndex ) ;
setIsStageRailOpen ( true ) ;
setIsStageRailOpen ( true ) ;
setIsWorkPanelOpen ( true ) ;
setIsWorkPanelOpen ( true ) ;
@@ -15060,6 +15269,20 @@ export function App() {
setAnalyzingProjectId ( project . id ) ;
setAnalyzingProjectId ( project . id ) ;
try {
try {
const currentKeywordMap = keywordMaps [ project . id ] ? ? null ;
const shouldCreateSafetySnapshot = Boolean ( currentKeywordMap && currentKeywordMap . items . length > 0 ) ;
let safetySnapshotLabel : string | null = null ;
if ( shouldCreateSafetySnapshot ) {
const safetySnapshot = await persistKeywordContextSnapshot (
project ,
"manual_save" ,
` Автоснимок перед новым анализом · ${ new Date ( ) . toLocaleString ( "ru-RU" ) } ` ,
"Safety snapshot: текущая карта ключей сохранена перед полным пересбором semantic/context/latest состояния проекта."
) ;
safetySnapshotLabel = safetySnapshot . label ;
}
const result = await runSemanticAnalysis ( project . id ) ;
const result = await runSemanticAnalysis ( project . id ) ;
setSemanticAnalyses ( ( currentAnalyses ) = > ( {
setSemanticAnalyses ( ( currentAnalyses ) = > ( {
@@ -15139,7 +15362,7 @@ export function App() {
} ) ) ;
} ) ) ;
invalidateStrategyQualityReview ( project . id ) ;
invalidateStrategyQualityReview ( project . id ) ;
setProjectActionMessage (
setProjectActionMessage (
` DEV context готов: ${ getModelTaskExecutionStatusLabel ( contextRun . runStatus ) } . Business synthesis: ${ getModelTaskExecutionStatusLabel (
` ${ safetySnapshotLabel ? ` Safety snapshot сохранён: ${ safetySnapshotLabel } . ` : "" } DEV context готов: ${ getModelTaskExecutionStatusLabel ( contextRun . runStatus ) } . Business synthesis: ${ getModelTaskExecutionStatusLabel (
businessRun . runStatus
businessRun . runStatus
) } . Commercial demand : $ { getModelTaskExecutionStatusLabel (
) } . Commercial demand : $ { getModelTaskExecutionStatusLabel (
commercialRun . runStatus
commercialRun . runStatus
@@ -16235,16 +16458,28 @@ export function App() {
< div className = "project-list" >
< div className = "project-list" >
{ visibleProjects . map ( ( project ) = > {
{ visibleProjects . map ( ( project ) = > {
const scanSummary = scanSummaries [ project . id ] ;
const scanSummary = scanSummaries [ project . id ] ;
const semanticAnalysis = semanticAnalyses [ project . id ] ? ? null ;
const projectKeywordContextSnapshotId =
project . id === activeProject ? . id ? activeKeywordContextSnapshotId : "current" ;
const projectKeywordContextSnapshotKey =
projectKeywordContextSnapshotId !== "current"
? getKeywordContextSnapshotDetailKey ( project . id , projectKeywordContextSnapshotId )
: null ;
const projectKeywordContextSnapshotDetail = projectKeywordContextSnapshotKey
? ( keywordContextSnapshotDetails [ projectKeywordContextSnapshotKey ] ? ? null )
: null ;
const semanticAnalysis =
projectKeywordContextSnapshotDetail ? . snapshot . semanticAnalysis ? ? ( semanticAnalyses [ project . id ] ? ? null ) ;
const contextReview = contextReviews [ project . id ] ? ? null ;
const contextReview = contextReviews [ project . id ] ? ? null ;
const isContextReviewAligned = Boolean (
const isContextReviewAligned = Boolean (
semanticAnalysis && contextReview && contextReview . semanticRunId === semanticAnalysis . runId
semanticAnalysis && contextReview && contextReview . semanticRunId === semanticAnalysis . runId
) ;
) ;
const activeContextReview = isContextReviewAligned ? contextReview : null ;
const activeContextReview = isContextReviewAligned ? contextReview : null ;
const projectOntology = projectOntologies [ project . id ] ? ? null ;
const projectOntology = projectOntologies [ project . id ] ? ? null ;
const marketEnrichment = marketEnrichments [ project . id ] ? ? null ;
const marketEnrichment =
const keywordCleaning = keywordCleanings [ project . id ] ? ? null ;
projectKeywordContextSnapshotDetail ? . snapshot . marketEnrichment ? ? ( marketEnrichments [ project . id ] ? ? null ) ;
const keywordMap = keywordMaps [ project . id ] ? ? null ;
const keywordCleaning =
projectKeywordContextSnapshotDetail ? . snapshot . keywordCleaning ? ? ( keywordCleanings [ project . id ] ? ? null ) ;
const keywordMap = projectKeywordContextSnapshotDetail ? . snapshot . keywordMap ? ? ( keywordMaps [ project . id ] ? ? null ) ;
const seoStrategy = seoStrategies [ project . id ] ? ? null ;
const seoStrategy = seoStrategies [ project . id ] ? ? null ;
const yandexEvidence = yandexEvidences [ project . id ] ? ? null ;
const yandexEvidence = yandexEvidences [ project . id ] ? ? null ;
const serpInterpretation = serpInterpretations [ project . id ] ? ? null ;
const serpInterpretation = serpInterpretations [ project . id ] ? ? null ;
@@ -16271,7 +16506,8 @@ export function App() {
const keywordAnalysisWorkflow = keywordAnalysisWorkflows [ project . id ] ? ? null ;
const keywordAnalysisWorkflow = keywordAnalysisWorkflows [ project . id ] ? ? null ;
const isKeywordAnalysisRebuilding = isKeywordAnalysisWorkflowActive ( keywordAnalysisWorkflow ) ;
const isKeywordAnalysisRebuilding = isKeywordAnalysisWorkflowActive ( keywordAnalysisWorkflow ) ;
const anchorReviewDraft = anchorReviewDraftDecisions [ project . id ] ? ? { } ;
const anchorReviewDraft = anchorReviewDraftDecisions [ project . id ] ? ? { } ;
const rawAnchorReview = marketEnrichment ? . anchorReview ? ? null ;
const rawAnchorReview =
projectKeywordContextSnapshotDetail ? . snapshot . anchorReview ? ? marketEnrichment ? . anchorReview ? ? null ;
const anchorReviewDraftCount = getAnchorReviewDraftDecisionList ( rawAnchorReview , anchorReviewDraft ) . length ;
const anchorReviewDraftCount = getAnchorReviewDraftDecisionList ( rawAnchorReview , anchorReviewDraft ) . length ;
const savedDemandCollectionProfile = rawAnchorReview ? . demandCollectionProfile ? ? "contextual" ;
const savedDemandCollectionProfile = rawAnchorReview ? . demandCollectionProfile ? ? "contextual" ;
const anchorReviewDemandProfile =
const anchorReviewDemandProfile =
@@ -16282,7 +16518,11 @@ export function App() {
const anchorReviewDraftChangeCount =
const anchorReviewDraftChangeCount =
anchorReviewDraftCount + ( isAnchorReviewDemandProfileDirty ? 1 : 0 ) ;
anchorReviewDraftCount + ( isAnchorReviewDemandProfileDirty ? 1 : 0 ) ;
const anchorReview = rawAnchorReview ? applyAnchorReviewDraft ( rawAnchorReview , anchorReviewDraft ) : null ;
const anchorReview = rawAnchorReview ? applyAnchorReviewDraft ( rawAnchorReview , anchorReviewDraft ) : null ;
const anchorReviewReady = Boolean ( marketEnrichment ? . readiness . anchorReviewReady ) ;
const anchorReviewReady = Boolean (
marketEnrichment ? . readiness . anchorReviewReady ||
rawAnchorReview ? . state === "approved" ||
( rawAnchorReview ? . readiness . wordstatQueueCount ? ? 0 ) > 0
) ;
const keywordAnalysisReady = Boolean (
const keywordAnalysisReady = Boolean (
! isKeywordAnalysisRebuilding &&
! isKeywordAnalysisRebuilding &&
( keywordAnalysisWorkflow
( keywordAnalysisWorkflow
@@ -16299,8 +16539,16 @@ export function App() {
const isCurationStageCollapsed = Boolean (
const isCurationStageCollapsed = Boolean (
collapsedKeywordStages [ getKeywordStageCollapseKey ( project . id , "curation" ) ]
collapsedKeywordStages [ getKeywordStageCollapseKey ( project . id , "curation" ) ]
) ;
) ;
const keywordCurationOverrides = keywordCurationRoleOverrides [ project . id ] ? ? { } ;
const keywordCurationOverrides = {
const keywordCurationSuppressedIds = keywordCurationSuppressedItemIds [ project . id ] ? ? [ ] ;
. . . getKeywordContextSnapshotOverrideRecord ( projectKeywordContextSnapshotDetail ) ,
. . . ( keywordCurationRoleOverrides [ project . id ] ? ? { } )
} ;
const keywordCurationSuppressedIds = Array . from (
new Set ( [
. . . getKeywordContextSnapshotSuppressedIds ( projectKeywordContextSnapshotDetail ) ,
. . . ( keywordCurationSuppressedItemIds [ project . id ] ? ? [ ] )
] )
) ;
const keywordCurationSuppressedSet = new Set ( keywordCurationSuppressedIds ) ;
const keywordCurationSuppressedSet = new Set ( keywordCurationSuppressedIds ) ;
const keywordCleaningStage5Candidates = keywordCleaning ? getKeywordCleaningStage5Candidates ( keywordCleaning ) : [ ] ;
const keywordCleaningStage5Candidates = keywordCleaning ? getKeywordCleaningStage5Candidates ( keywordCleaning ) : [ ] ;
const keywordPlanDecision = keywordMap ? getKeywordPlanDecision ( keywordMap ) : null ;
const keywordPlanDecision = keywordMap ? getKeywordPlanDecision ( keywordMap ) : null ;