Kaynağa Gözat

Structured competitive profile schema (pricing, features, channels, ICP)

Extends the /competitors/:id/summarize AI prompt to extract a structured
profile block alongside the existing analysis: pricing model, 3-5 key
features, marketing channels used, and target customer description.

The profile fields are validated and stored in aiAnalysis.profile. A compact
fact-sheet renders in the competitor card below the freshness row and above
the full analysis, giving an at-a-glance comparison across all tracked
competitors. Re-running "Summarise with AI" on existing competitors populates
the new fields.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Benjamin Harris 3 hafta önce
ebeveyn
işleme
68dafdcd07

+ 13 - 1
services/gateway/server.js

@@ -2722,7 +2722,13 @@ app.post('/competitors/:id/summarize', async (request, reply) => {
   "tone": "one sentence describing their voice and communication style",
   "positioning": "one sentence on how they position themselves in the market",
   "gaps": ["2-3 topics or angles they ignore or handle poorly — opportunities for you"],
-  "moves": ["3 specific content angles you could use to stand out against them"]
+  "moves": ["3 specific content angles you could use to stand out against them"],
+  "profile": {
+    "pricing": "one sentence on their pricing model or tier (e.g. 'Freemium with $49/mo Pro plan', 'Premium only, starts at $99/mo', or 'Pricing not visible' if unclear)",
+    "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"
+  }
 }
 
 Content:
@@ -2765,6 +2771,12 @@ Return ONLY the JSON object. No explanation, no markdown.`;
       if (typeof aiAnalysis.positioning !== 'string') aiAnalysis.positioning = '';
       if (!Array.isArray(aiAnalysis.gaps)) aiAnalysis.gaps = [];
       if (!Array.isArray(aiAnalysis.moves)) aiAnalysis.moves = [];
+      // Validate profile block
+      if (!aiAnalysis.profile || typeof aiAnalysis.profile !== 'object') aiAnalysis.profile = {};
+      if (typeof aiAnalysis.profile.pricing !== 'string') aiAnalysis.profile.pricing = '';
+      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 = '';
     } catch {
       aiAnalysis = null;
     }

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

@@ -501,6 +501,10 @@ export default {
     intent_transactional: 'Transactional',
     intent_navigational: 'Navigational',
     sideBySideMode: 'Comparing competitors side by side',
+    profilePricing: 'Pricing',
+    profileTarget: 'Target customer',
+    profileFeatures: 'Key features',
+    profileChannels: 'Channels',
     discoverButton: 'Find Competitors Automatically',
     discovering: 'Discovering…',
     discoverySuggestionsLabel: 'AI-suggested competitors — click Add to track them:',

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

@@ -501,6 +501,10 @@ export default {
     intent_transactional: 'İşlemsel',
     intent_navigational: 'Yönlendirici',
     sideBySideMode: 'Rakipler yan yana karşılaştırılıyor',
+    profilePricing: 'Fiyatlandırma',
+    profileTarget: 'Hedef müşteri',
+    profileFeatures: 'Önemli özellikler',
+    profileChannels: 'Kanallar',
     discoverButton: 'Rakipleri Otomatik Bul',
     discovering: 'Aranıyor…',
     discoverySuggestionsLabel: 'YZ tarafından önerilen rakipler — eklemek için Ekle\'ye tıklayın:',

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

@@ -2,12 +2,20 @@ import { defineStore } from 'pinia'
 import { ref } from 'vue'
 import axios from 'axios'
 
+export interface CompetitorProfile {
+  pricing: string
+  keyFeatures: string[]
+  marketingChannels: string[]
+  targetCustomer: string
+}
+
 export interface AiAnalysis {
   themes: string[]
   tone: string
   positioning: string
   gaps: string[]
   moves: string[]
+  profile?: CompetitorProfile
 }
 
 export type KeywordIntent = 'informational' | 'commercial' | 'transactional' | 'navigational'

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

@@ -144,6 +144,26 @@
           </span>
         </div>
 
+        <!-- Structured Profile Fact-sheet -->
+        <div v-if="competitor.aiAnalysis?.profile?.pricing || competitor.aiAnalysis?.profile?.keyFeatures?.length" class="mb-3 p-3 bg-gray-900/50 border border-gray-700/60 rounded-lg text-xs space-y-1.5">
+          <div v-if="competitor.aiAnalysis.profile.pricing" class="flex gap-1.5">
+            <span class="text-gray-500 shrink-0">{{ t('competitors.profilePricing') }}:</span>
+            <span class="text-gray-300">{{ competitor.aiAnalysis.profile.pricing }}</span>
+          </div>
+          <div v-if="competitor.aiAnalysis.profile.targetCustomer" class="flex gap-1.5">
+            <span class="text-gray-500 shrink-0">{{ t('competitors.profileTarget') }}:</span>
+            <span class="text-gray-300">{{ competitor.aiAnalysis.profile.targetCustomer }}</span>
+          </div>
+          <div v-if="competitor.aiAnalysis.profile.keyFeatures?.length" class="flex gap-1.5 flex-wrap">
+            <span class="text-gray-500 shrink-0">{{ t('competitors.profileFeatures') }}:</span>
+            <span v-for="f in competitor.aiAnalysis.profile.keyFeatures" :key="f" class="px-1.5 py-0.5 bg-gray-700 text-gray-300 rounded">{{ f }}</span>
+          </div>
+          <div v-if="competitor.aiAnalysis.profile.marketingChannels?.length" class="flex gap-1.5 flex-wrap">
+            <span class="text-gray-500 shrink-0">{{ t('competitors.profileChannels') }}:</span>
+            <span v-for="c in competitor.aiAnalysis.profile.marketingChannels" :key="c" class="px-1.5 py-0.5 bg-gray-700 text-gray-300 rounded">{{ c }}</span>
+          </div>
+        </div>
+
         <!-- Structured AI Analysis -->
         <div v-if="competitor.aiAnalysis" class="mt-3 space-y-3">
           <!-- Tone & Positioning -->