Jelajahi Sumber

Competitor response prediction using Porter's four-component framework

Extends /competitors/:id/summarize to extract a prediction block alongside
the existing analysis: satisfiedWithPosition (boolean), likelyNextMoves (2-3
strategic moves), vulnerabilities (2-3 exploitable blind spots), and
retaliationTriggers (1-2 moves that would provoke a strong response).

The prediction block shifts analysis from descriptive ("what they are doing")
to predictive ("what they will do"), grounded in the observable evidence in
their scraped content. Existing competitors need "Summarise with AI" re-run
to populate the new prediction fields.

UI renders the prediction in a sky/blue tinted panel inside the AI Analysis
section: a "Holding position" or "Actively pushing" status badge, then three
sub-lists for next moves (sky), vulnerabilities (amber), and retaliation
triggers (red).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Benjamin Harris 3 minggu lalu
induk
melakukan
379b68e4b6

+ 12 - 0
services/gateway/server.js

@@ -2728,6 +2728,12 @@ app.post('/competitors/:id/summarize', async (request, reply) => {
     "keyFeatures": ["3-5 core product or service features they emphasise"],
     "marketingChannels": ["2-4 social/marketing channels they actively use based on content"],
     "targetCustomer": "one sentence describing their apparent ideal customer"
+  },
+  "prediction": {
+    "satisfiedWithPosition": <true if they appear content with their current position, false if they seem to be pushing for growth>,
+    "likelyNextMoves": ["2-3 strategic moves they will probably make based on current trajectory"],
+    "vulnerabilities": ["2-3 specific weaknesses or blind spots visible in their content"],
+    "retaliationTriggers": ["1-2 moves by a competitor that would most likely provoke a strong response from them"]
   }
 }
 
@@ -2777,6 +2783,12 @@ Return ONLY the JSON object. No explanation, no markdown.`;
       if (!Array.isArray(aiAnalysis.profile.keyFeatures)) aiAnalysis.profile.keyFeatures = [];
       if (!Array.isArray(aiAnalysis.profile.marketingChannels)) aiAnalysis.profile.marketingChannels = [];
       if (typeof aiAnalysis.profile.targetCustomer !== 'string') aiAnalysis.profile.targetCustomer = '';
+      // Validate prediction block
+      if (!aiAnalysis.prediction || typeof aiAnalysis.prediction !== 'object') aiAnalysis.prediction = {};
+      if (typeof aiAnalysis.prediction.satisfiedWithPosition !== 'boolean') aiAnalysis.prediction.satisfiedWithPosition = true;
+      if (!Array.isArray(aiAnalysis.prediction.likelyNextMoves)) aiAnalysis.prediction.likelyNextMoves = [];
+      if (!Array.isArray(aiAnalysis.prediction.vulnerabilities)) aiAnalysis.prediction.vulnerabilities = [];
+      if (!Array.isArray(aiAnalysis.prediction.retaliationTriggers)) aiAnalysis.prediction.retaliationTriggers = [];
     } catch {
       aiAnalysis = null;
     }

+ 6 - 0
ui/src/locales/en.ts

@@ -501,6 +501,12 @@ export default {
     intent_transactional: 'Transactional',
     intent_navigational: 'Navigational',
     sideBySideMode: 'Comparing competitors side by side',
+    predictionLabel: 'Response Prediction',
+    predictionSatisfied: 'Holding position',
+    predictionPushing: 'Actively pushing',
+    predictionNextMoves: 'Likely next moves',
+    predictionVulnerabilities: 'Exploitable vulnerabilities',
+    predictionRetaliationTriggers: 'Retaliation triggers',
     detectSignals: 'Detect Signals',
     detectingSignals: 'Detecting…',
     signalsLabel: 'Market Signals',

+ 6 - 0
ui/src/locales/tr.ts

@@ -501,6 +501,12 @@ export default {
     intent_transactional: 'İşlemsel',
     intent_navigational: 'Yönlendirici',
     sideBySideMode: 'Rakipler yan yana karşılaştırılıyor',
+    predictionLabel: 'Yanıt Tahmini',
+    predictionSatisfied: 'Pozisyonunu koruyor',
+    predictionPushing: 'Aktif büyüme peşinde',
+    predictionNextMoves: 'Muhtemel sonraki hamleler',
+    predictionVulnerabilities: 'Sömürülebilir zayıflıklar',
+    predictionRetaliationTriggers: 'Misilleme tetikleyicileri',
     detectSignals: 'Sinyalleri Algıla',
     detectingSignals: 'Algılanıyor…',
     signalsLabel: 'Pazar Sinyalleri',

+ 8 - 0
ui/src/stores/competitors.ts

@@ -18,6 +18,13 @@ export interface CompetitorSignal {
   detectedAt: string
 }
 
+export interface CompetitorPrediction {
+  satisfiedWithPosition: boolean
+  likelyNextMoves: string[]
+  vulnerabilities: string[]
+  retaliationTriggers: string[]
+}
+
 export interface AiAnalysis {
   themes: string[]
   tone: string
@@ -25,6 +32,7 @@ export interface AiAnalysis {
   gaps: string[]
   moves: string[]
   profile?: CompetitorProfile
+  prediction?: CompetitorPrediction
 }
 
 export type KeywordIntent = 'informational' | 'commercial' | 'transactional' | 'navigational'

+ 34 - 0
ui/src/views/Competitors.vue

@@ -249,6 +249,40 @@
               </li>
             </ul>
           </div>
+
+          <!-- Response Prediction -->
+          <div v-if="competitor.aiAnalysis.prediction?.likelyNextMoves?.length" class="p-3 bg-gray-700/30 rounded border border-gray-700 space-y-2">
+            <div class="flex items-center gap-2">
+              <div class="text-xs text-sky-400 font-medium">{{ t('competitors.predictionLabel') }}</div>
+              <span class="text-xs px-1.5 py-0.5 rounded" :class="competitor.aiAnalysis.prediction.satisfiedWithPosition ? 'bg-gray-700 text-gray-400' : 'bg-orange-900/40 text-orange-300'">
+                {{ competitor.aiAnalysis.prediction.satisfiedWithPosition ? t('competitors.predictionSatisfied') : t('competitors.predictionPushing') }}
+              </span>
+            </div>
+            <div v-if="competitor.aiAnalysis.prediction.likelyNextMoves?.length">
+              <div class="text-xs text-gray-400 mb-1">{{ t('competitors.predictionNextMoves') }}</div>
+              <ul class="space-y-0.5">
+                <li v-for="move in competitor.aiAnalysis.prediction.likelyNextMoves" :key="move" class="text-xs text-sky-200 flex gap-1.5">
+                  <span class="text-sky-400 shrink-0">→</span>{{ move }}
+                </li>
+              </ul>
+            </div>
+            <div v-if="competitor.aiAnalysis.prediction.vulnerabilities?.length">
+              <div class="text-xs text-gray-400 mb-1">{{ t('competitors.predictionVulnerabilities') }}</div>
+              <ul class="space-y-0.5">
+                <li v-for="v in competitor.aiAnalysis.prediction.vulnerabilities" :key="v" class="text-xs text-amber-200 flex gap-1.5">
+                  <span class="text-amber-400 shrink-0">⚡</span>{{ v }}
+                </li>
+              </ul>
+            </div>
+            <div v-if="competitor.aiAnalysis.prediction.retaliationTriggers?.length">
+              <div class="text-xs text-gray-400 mb-1">{{ t('competitors.predictionRetaliationTriggers') }}</div>
+              <ul class="space-y-0.5">
+                <li v-for="rt in competitor.aiAnalysis.prediction.retaliationTriggers" :key="rt" class="text-xs text-red-300 flex gap-1.5">
+                  <span class="text-red-400 shrink-0">⚠</span>{{ rt }}
+                </li>
+              </ul>
+            </div>
+          </div>
         </div>
 
         <!-- Legacy plain-text summary (for competitors analysed before this update) -->