/** * TypeScript mirrors of the Pydantic models on the Recomaze agent backend * (``api/schemas/ai_visibility/*``). Field names and casing match the JSON * on the wire. Numeric percentage / score fields are already rounded on * the backend - **do not** re-round in the UI. * * Many text fields come back pre-localized in the brand language (noted on * each field). Those must be rendered verbatim - never re-format. */ /** Standard response envelope used by every visibility endpoint. */ export interface VisibilityBaseResponse { /** Human message (English, unused by the UI). */ message: string; /** HTTP status mirrored in the body. */ status: number; /** ``true`` when ``data`` should be treated as an error payload. */ errors: boolean; /** Parsed response body. */ data: T; } /* ------------------------------------------------------------------ */ /* settings */ /* ------------------------------------------------------------------ */ /** Merchant-wide visibility settings (``GET /settings``). */ export interface VisibilitySettingsResponse { client_id: string; preferred_language: string; default_country: string | null; default_engines: string[]; detected_from: 'domain' | 'gemini' | 'user'; slack_channel: string | null; created_at: string | null; updated_at: string | null; } /** Body for ``PUT /settings`` - every field optional (partial update). */ export interface VisibilitySettingsUpdateRequest { preferred_language?: string; default_country?: string; default_engines?: string[]; slack_channel?: string; } /* ------------------------------------------------------------------ */ /* brand */ /* ------------------------------------------------------------------ */ export type BrandStatus = 'active' | 'paused'; export interface BrandCreateRequest { domain: string; brand_name?: string; language?: string; country?: string; business_type?: string; niche?: string; differentiators?: string[]; keywords?: string[]; /** Optional business context that grounds prompt + keyword generation. */ knowledge_base?: string; } export interface BrandUpdateRequest { brand_name?: string; language?: string; country?: string; status?: BrandStatus; /** e.g. ``ecommerce``, ``saas``, ``agency``. */ business_type?: string; /** Free-text niche, e.g. ``running shoes``, ``B2B accounting software``. */ niche?: string; differentiators?: string[]; keywords?: string[]; /** Optional business context that grounds prompt + keyword generation. */ knowledge_base?: string; } export interface ManualCompetitor { domain: string; name: string | null; } export interface BrandResponse { client_id: string; brand_id: string; brand_name: string | null; domain: string | null; language: string; country: string | null; business_type: string | null; niche: string | null; differentiators: string[]; keywords: string[]; /** Optional merchant business context that grounds prompt generation. */ knowledge_base: string | null; manual_competitors: ManualCompetitor[]; status: BrandStatus; last_scan_at: string | null; created_at: string | null; updated_at: string | null; } export interface BrandListResponse { brands: BrandResponse[]; } export interface ManualCompetitorAddRequest { domain: string; name?: string; } export interface ManualCompetitorListResponse { competitors: ManualCompetitor[]; } /** Body for ``POST /brands/{id}/excluded-competitors``. */ export interface ExcludedCompetitorAddRequest { /** Canonical domain to suppress from the leaderboard and future scans. */ domain: string; } /** * Response for GET/POST/DELETE on ``/brands/{id}/excluded-competitors``: * backend always echoes the current excluded-domains list so the UI never * needs a second round-trip to refresh. */ export interface ExcludedCompetitorListResponse { /** Canonical domains the merchant has excluded. */ domains: string[]; } /* ------------------------------------------------------------------ */ /* scan */ /* ------------------------------------------------------------------ */ export interface ScanTriggerRequest { language?: string; country?: string; queries_per_category?: number; custom_queries?: string[]; } export interface ScanDispatchResponse { job_id: string; status: string; poll_url: string; } export interface ScanHistoryPoint { date: string; week_iso: string | null; visibility_score: number; share_of_voice: number | null; queries_checked: number; brand_mentions: number; } export interface ScanHistoryResponse { history: ScanHistoryPoint[]; trend: string; trend_percentage: number; } /* ------------------------------------------------------------------ */ /* weekly report */ /* ------------------------------------------------------------------ */ export interface VisibilityProblem { id: string; type: string; reason_code: string | null; reason: string; reason_localized: string | null; type_label: string | null; severity_label: string | null; severity: 'low' | 'medium' | 'high' | 'critical' | 'warn' | 'info' | string; impact_score: number; metrics: Record; extra: Record; fix?: { problem_id?: string | null; problem_type?: string | null; title: string; severity: string; impact_score: number; steps: string[]; suggested_article?: { title: string; topic?: string | null; target_prompts: string[]; keywords: string[]; } | null; resources: { label: string; url: string }[]; } | null; } export interface RecommendedArticleStub { title?: string | null; topic?: string | null; target_prompts?: string[]; keywords?: string[]; article_id?: string | null; priority?: string | null; [key: string]: unknown; } export interface WeeklyReportResponse { week_iso: string; visibility_score: number; delta_score: number | null; share_of_voice: number; delta_sov: number | null; /** * Number of monitored prompts where the merchant's own brand was * mentioned this week. Surfaces in the competitors-table own-brand * row so the "Mentions" cell shows the real count instead of a dash. */ brand_mentions: number; sentiment_positive_pct: number | null; sentiment_negative_pct: number | null; delta_sentiment: number | null; brand_rank_overall: number | null; total_competing_entities: number | null; top_wins: string[]; top_losses: string[]; new_competitors: Record[]; lost_citations: Record[]; problems: VisibilityProblem[]; recommended_articles: RecommendedArticleStub[]; generated_at: string | null; } /* ------------------------------------------------------------------ */ /* competitors */ /* ------------------------------------------------------------------ */ export interface CompetitorSnapshot { competitor_domain: string; competitor_name: string | null; mentions_count: number; share_of_voice: number; avg_position: number | null; delta_vs_prev: number; sample_queries: string[]; manually_tracked: boolean; } export interface CompetitorLeaderboardResponse { week_iso: string; competitors: CompetitorSnapshot[]; } /* ------------------------------------------------------------------ */ /* prompts */ /* ------------------------------------------------------------------ */ export type PromptCategory = 'product' | 'scenario' | 'best_of' | 'geo'; export interface PromptResponse { prompt_id: string; brand_id: string; client_id: string; prompt_text: string; category: PromptCategory; language: string; enabled: boolean; source: string; created_at: string | null; updated_at: string | null; } export interface PromptListResponse { prompts: PromptResponse[]; } export interface PromptInput { text: string; category?: PromptCategory; enabled?: boolean; } export interface PromptReplaceRequest { prompts: PromptInput[]; } /** Body for ``PATCH /brands/{id}/prompts/{prompt_id}`` - single-prompt rewrite. */ export interface PromptUpdateRequest { /** New prompt body, in the brand's language. Backend rejects fewer than 3 chars. */ text: string; } export interface PromptResponseEntry { engine: string; response_snippet: string | null; brand_mentioned: boolean; brand_position: number | null; sentiment: string | null; sources: string[]; scan_date: string | null; } export interface PromptResponsesResponse { prompt_id: string; prompt_text: string | null; entries: PromptResponseEntry[]; } /* ------------------------------------------------------------------ */ /* problems (standalone endpoint, independent of weekly report) */ /* ------------------------------------------------------------------ */ export interface ProblemsResponse { week_iso: string | null; scan_date: string | null; problems: VisibilityProblem[]; } /* ------------------------------------------------------------------ */ /* suggestions */ /* ------------------------------------------------------------------ */ export type SuggestionLifecycle = | 'suggested' | 'accepted' | 'generating' | 'generated' | 'published' | 'monitoring' | 'closed' | 'rejected'; export interface SuggestionEntry { suggestion_id: string; brand_id: string; title: string; topic: string | null; keywords: string[]; target_prompts: string[]; estimated_word_count: number; rationale: string | null; priority_score: number; source: string; source_problem_id: string | null; language: string; lifecycle_state: SuggestionLifecycle; closing_week_iso: string | null; created_at: string | null; updated_at: string | null; } export interface SuggestionListResponse { suggestions: SuggestionEntry[]; } export interface RefreshSuggestionsRequest { min_count?: number; replace_existing?: boolean; } /* ------------------------------------------------------------------ */ /* articles */ /* ------------------------------------------------------------------ */ export type ArticleStatus = | 'pending' | 'generating' | 'ready' | 'failed' | 'published'; export type ArticleTrigger = 'manual' | 'bulk' | 'problem_fix' | 'suggestion'; export type EditedBy = 'ai' | 'user' | 'bulk_regen'; export interface ArticleGenerateRequest { brand_id: string; topic: string; target_prompts?: string[]; keywords?: string[]; language?: string; suggestion_id?: string; problem_id?: string; triggered_by?: ArticleTrigger; } export interface ArticleDispatchResponse { job_id: string; article_id: string; poll_url: string; } export interface ArticleSummary { client_id: string; brand_id: string; article_id: string; title: string; slug: string | null; language: string; status: ArticleStatus; word_count: number; seo_score: number; aeo_score: number; triggered_by: ArticleTrigger; triggered_by_suggestion_id: string | null; triggered_by_problem_id: string | null; created_at: string | null; updated_at: string | null; /** ISO timestamp of the moment the merchant marked the article as live. */ published_at: string | null; } /** * Aggregate counts the dashboard renders next to the Articles tab labels. * ``recommended`` covers articles still in flight or ready-but-not-published * (``pending`` / ``generating`` / ``ready``); ``published`` covers articles * the merchant flipped to live. */ export interface ArticleCounts { recommended: number; published: number; } export interface ArticleListResponse { articles: ArticleSummary[]; counts: ArticleCounts; } export interface ArticleDetailResponse { summary: ArticleSummary; meta_description: string | null; h1: string | null; outline: Record; markdown: string | null; sections: Record[]; faq: { question: string; answer: string }[]; schema_article_jsonld: string | null; schema_faq_jsonld: string | null; internal_links: Record[]; external_refs: Record[]; image_suggestions: Record[]; target_prompts: string[]; } export interface ArticleEditRequest { markdown?: string; title?: string; meta_description?: string; h1?: string; } /** Response payload for ``DELETE /articles/{id}`` - hard-delete + version cascade. */ export interface ArticleDeleteResponse { article_id: string; versions_deleted: number; } /** * Body for ``POST /articles/{id}/regenerate``. Optional ``instructions`` * is a free-form merchant directive threaded into the LLM prompt as the * highest-priority constraint - tone shifts, structure changes (listicle * / how-to / comparison), word exclusions, or new sections to add. Hard * cap at 2000 chars on the backend. */ export interface ArticleRegenerateRequest { instructions?: string; } export interface ArticleVersionEntry { version_id: string; edited_by: EditedBy; diff_summary: string | null; created_at: string | null; } export interface ArticleVersionsResponse { versions: ArticleVersionEntry[]; } export interface ArticleVersionDetailResponse { article_id: string; version_id: string; title: string | null; edited_by: EditedBy; diff_summary: string | null; markdown: string; created_at: string | null; } /* ------------------------------------------------------------------ */ /* bulk articles */ /* ------------------------------------------------------------------ */ export type BulkStatus = | 'pending' | 'running' | 'completed' | 'partial_failed' | 'failed'; export interface BulkGenerateRequest { brand_id: string; suggestion_ids: string[]; } export interface BulkJobResponse { client_id: string; brand_id: string; job_id: string; requested_count: number; completed_count: number; failed_count: number; status: BulkStatus; suggestion_ids: string[]; article_ids: string[]; total_cost_usd: number; started_at: string | null; finished_at: string | null; created_at: string | null; } /* ------------------------------------------------------------------ */ /* chat-prompt mining */ /* ------------------------------------------------------------------ */ export interface ChatPromptVolumeEntry { prompt_theme_id: string; theme_label: string; sample_questions: string[]; volume_7d: number; volume_30d: number; volume_90d: number; trending_score: number; language: string; mapped_visibility_prompt_id: string | null; first_seen: string | null; last_seen: string | null; } export interface ChatPromptVolumesResponse { themes: ChatPromptVolumeEntry[]; } export interface AdoptFromChatRequest { theme_ids: string[]; } export interface AdoptFromChatResponse { adopted: Record[]; } export interface StopTrackingChatRequest { theme_ids: string[]; } export interface StopTrackingChatEntry { theme_id: string; deleted_prompt_ids: string[]; deleted: boolean; } export interface StopTrackingChatResponse { untracked: StopTrackingChatEntry[]; } /* ------------------------------------------------------------------ */ /* errors */ /* ------------------------------------------------------------------ */ /** * Thrown by {@link startScan} when the backend returns ``402`` or a body * containing ``ScanBudgetExceeded``. UI renders a warning banner rather * than a red toast in this case. */ export class ScanBudgetError extends Error { readonly isScanBudgetError = true; constructor(message = "You've reached this week's scan budget.") { super(message); this.name = 'ScanBudgetError'; } }