competitors.ts 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258
  1. import { defineStore } from 'pinia'
  2. import { ref } from 'vue'
  3. import axios from 'axios'
  4. export interface AiAnalysis {
  5. themes: string[]
  6. tone: string
  7. positioning: string
  8. gaps: string[]
  9. moves: string[]
  10. }
  11. export type KeywordIntent = 'informational' | 'commercial' | 'transactional' | 'navigational'
  12. export interface CompetitorKeyword {
  13. term: string
  14. intent: KeywordIntent
  15. extractedAt?: string
  16. }
  17. export interface GapItem {
  18. term: string
  19. intent: KeywordIntent
  20. }
  21. export interface CoveredItem extends GapItem {
  22. matchedHashtags: string[]
  23. }
  24. export interface GapAnalysis {
  25. gaps: GapItem[]
  26. covered: CoveredItem[]
  27. totalKeywords: number
  28. hashtagStatsEmpty: boolean
  29. lastAnalyzed: string
  30. }
  31. export interface RoadmapPost {
  32. topic: string
  33. headline: string
  34. keywords: string[]
  35. rationale: string
  36. }
  37. export interface CompetitorSuggestion {
  38. name: string
  39. websiteUrl: string
  40. reason: string
  41. }
  42. export interface Competitor {
  43. _id: string
  44. name: string
  45. websiteUrl: string
  46. socialUrls: Partial<Record<string, string>>
  47. scrapedContent: { source: string; url: string; text: string; scrapedAt: string }[]
  48. aiSummary: string
  49. aiAnalysis?: AiAnalysis
  50. keywords: CompetitorKeyword[]
  51. contentChanged?: boolean
  52. gapAnalysis?: GapAnalysis
  53. contentRoadmap?: RoadmapPost[]
  54. lastScraped: string | null
  55. createdAt: string
  56. updatedAt: string
  57. }
  58. export const useCompetitorStore = defineStore('competitors', () => {
  59. const competitors = ref<Competitor[]>([])
  60. const loading = ref(false)
  61. const scraping = ref<Record<string, boolean>>({})
  62. const summarizing = ref<Record<string, boolean>>({})
  63. const extractingKeywords = ref<Record<string, boolean>>({})
  64. const analyzingGaps = ref<Record<string, boolean>>({})
  65. const generatingRoadmap = ref<Record<string, boolean>>({})
  66. const scrapeResults = ref<Record<string, { sources: number; ok: boolean; message: string }>>({})
  67. const error = ref<string | null>(null)
  68. async function fetchCompetitors() {
  69. loading.value = true
  70. error.value = null
  71. try {
  72. const res = await axios.get('/api/competitors')
  73. competitors.value = res.data
  74. } catch (err: any) {
  75. error.value = err.response?.data?.error || 'Failed to load competitors'
  76. } finally {
  77. loading.value = false
  78. }
  79. }
  80. async function addCompetitor(data: { name: string; websiteUrl: string; socialUrls?: Record<string, string> }): Promise<boolean> {
  81. error.value = null
  82. try {
  83. const res = await axios.post('/api/competitors', data)
  84. competitors.value.push(res.data)
  85. return true
  86. } catch (err: any) {
  87. error.value = err.response?.data?.error || 'Failed to add competitor'
  88. return false
  89. }
  90. }
  91. async function updateCompetitor(id: string, data: Partial<Pick<Competitor, 'name' | 'websiteUrl' | 'socialUrls'>>): Promise<boolean> {
  92. error.value = null
  93. try {
  94. const res = await axios.put(`/api/competitors/${id}`, data)
  95. const idx = competitors.value.findIndex((c) => c._id === id)
  96. if (idx !== -1) competitors.value[idx] = res.data
  97. return true
  98. } catch (err: any) {
  99. error.value = err.response?.data?.error || 'Failed to update competitor'
  100. return false
  101. }
  102. }
  103. async function deleteCompetitor(id: string): Promise<boolean> {
  104. error.value = null
  105. try {
  106. await axios.delete(`/api/competitors/${id}`)
  107. competitors.value = competitors.value.filter((c) => c._id !== id)
  108. return true
  109. } catch (err: any) {
  110. error.value = err.response?.data?.error || 'Failed to delete competitor'
  111. return false
  112. }
  113. }
  114. async function pollScrapeJob(competitorId: string, jobId: string): Promise<{ ok: boolean; sources: number; message: string }> {
  115. return new Promise((resolve) => {
  116. const check = async () => {
  117. try {
  118. const res = await axios.get(`/api/competitors/${competitorId}/scrape-status/${jobId}`)
  119. const { status, sources, message } = res.data
  120. if (status === 'done' || status === 'failed') {
  121. resolve({ ok: status === 'done', sources: sources ?? 0, message: message || '' })
  122. } else {
  123. setTimeout(check, 2000)
  124. }
  125. } catch {
  126. resolve({ ok: false, sources: 0, message: 'Status check failed' })
  127. }
  128. }
  129. check()
  130. })
  131. }
  132. async function scrapeCompetitor(id: string): Promise<void> {
  133. scraping.value = { ...scraping.value, [id]: true }
  134. try {
  135. const res = await axios.post(`/api/competitors/${id}/scrape`)
  136. const { jobId } = res.data
  137. const result = await pollScrapeJob(id, jobId)
  138. scrapeResults.value = { ...scrapeResults.value, [id]: result }
  139. await fetchCompetitors()
  140. } catch (err: any) {
  141. const msg = err.response?.data?.detail || err.response?.data?.error || err.message
  142. scrapeResults.value = { ...scrapeResults.value, [id]: { sources: 0, ok: false, message: msg } }
  143. } finally {
  144. scraping.value = { ...scraping.value, [id]: false }
  145. }
  146. }
  147. async function summarizeCompetitor(id: string): Promise<void> {
  148. summarizing.value = { ...summarizing.value, [id]: true }
  149. error.value = null
  150. try {
  151. const res = await axios.post(`/api/competitors/${id}/summarize`)
  152. const idx = competitors.value.findIndex((c) => c._id === id)
  153. if (idx !== -1) {
  154. competitors.value[idx].aiAnalysis = res.data.aiAnalysis
  155. competitors.value[idx].aiSummary = ''
  156. }
  157. } catch (err: any) {
  158. // Long AI calls can exceed the proxy timeout while the server still completes.
  159. // Re-fetch before showing an error — if the data saved, surface it silently.
  160. await fetchCompetitors()
  161. const saved = competitors.value.find((c) => c._id === id)?.aiAnalysis
  162. if (!saved) {
  163. error.value = err.response?.data?.detail || err.response?.data?.error || 'Summarization failed'
  164. }
  165. } finally {
  166. summarizing.value = { ...summarizing.value, [id]: false }
  167. }
  168. }
  169. async function extractKeywords(id: string): Promise<void> {
  170. extractingKeywords.value = { ...extractingKeywords.value, [id]: true }
  171. error.value = null
  172. try {
  173. const res = await axios.post(`/api/competitors/${id}/extract-keywords`)
  174. const idx = competitors.value.findIndex((c) => c._id === id)
  175. if (idx !== -1) competitors.value[idx].keywords = res.data.keywords || []
  176. } catch (err: any) {
  177. error.value = err.response?.data?.detail || err.response?.data?.error || 'Keyword extraction failed'
  178. } finally {
  179. extractingKeywords.value = { ...extractingKeywords.value, [id]: false }
  180. }
  181. }
  182. async function analyzeGaps(id: string): Promise<void> {
  183. analyzingGaps.value = { ...analyzingGaps.value, [id]: true }
  184. error.value = null
  185. try {
  186. const res = await axios.post(`/api/competitors/${id}/analyze-gaps`)
  187. const idx = competitors.value.findIndex((c) => c._id === id)
  188. if (idx !== -1) competitors.value[idx].gapAnalysis = res.data
  189. } catch (err: any) {
  190. error.value = err.response?.data?.detail || err.response?.data?.error || 'Gap analysis failed'
  191. } finally {
  192. analyzingGaps.value = { ...analyzingGaps.value, [id]: false }
  193. }
  194. }
  195. async function generateRoadmap(id: string): Promise<void> {
  196. generatingRoadmap.value = { ...generatingRoadmap.value, [id]: true }
  197. error.value = null
  198. try {
  199. const res = await axios.post(`/api/competitors/${id}/content-roadmap`)
  200. const idx = competitors.value.findIndex((c) => c._id === id)
  201. if (idx !== -1) competitors.value[idx].contentRoadmap = res.data.contentRoadmap
  202. } catch (err: any) {
  203. // Long AI calls can exceed the proxy timeout while the server still completes.
  204. // Re-fetch before showing an error — if the data saved, surface it silently.
  205. await fetchCompetitors()
  206. const saved = competitors.value.find((c) => c._id === id)?.contentRoadmap
  207. if (!saved?.length) {
  208. error.value = err.response?.data?.detail || err.response?.data?.error || 'Roadmap generation failed'
  209. }
  210. } finally {
  211. generatingRoadmap.value = { ...generatingRoadmap.value, [id]: false }
  212. }
  213. }
  214. const discoveringCompetitors = ref(false)
  215. const discoverySuggestions = ref<CompetitorSuggestion[]>([])
  216. async function discoverCompetitors(): Promise<void> {
  217. discoveringCompetitors.value = true
  218. error.value = null
  219. discoverySuggestions.value = []
  220. try {
  221. const res = await axios.post('/api/competitors/discover')
  222. discoverySuggestions.value = res.data.suggestions || []
  223. } catch (err: any) {
  224. error.value = err.response?.data?.detail || err.response?.data?.error || 'Discovery failed'
  225. } finally {
  226. discoveringCompetitors.value = false
  227. }
  228. }
  229. return {
  230. competitors, loading, scraping, summarizing, extractingKeywords, analyzingGaps, generatingRoadmap, scrapeResults,
  231. discoveringCompetitors, discoverySuggestions, error,
  232. fetchCompetitors, addCompetitor, updateCompetitor, deleteCompetitor,
  233. scrapeCompetitor, summarizeCompetitor, extractKeywords, analyzeGaps, generateRoadmap, discoverCompetitors,
  234. }
  235. })