Owner: Engineering Team | Last Updated: 2026-01-30 | Status: Current
The AI detector analyzes text to determine the probability it was generated by AI. It queries the backend which runs the text against 9 external detection services and returns individual + aggregate scores. The detector is accessible both as a standalone feature and inline within the humanizer workflow (the "Scan" button).
User clicks "Scan for AI"
│
▼
DetectorButton.tsx
├── Validates word count (min 50, max per-plan limit)
├── Checks credit balance (user.credit + referral.credits)
├── Sanitizes input text
└── Calls detector() via axios
│
▼
POST /api/feature/detector/
│
▼
Backend runs 9 external detectors
│
▼
Returns: { result, items, ai_score, credit }
│
▼
DetectorScorecard.tsx displays results
└── AIDetectorResult.tsx per-detector row
| Component | File | Purpose |
|---|---|---|
| Detector (button) | components/partials/humanizer/DetectorButton.tsx |
"Scan for AI" button with validation, API call, error handling |
| AIDetectorResult | components/partials/humanizer/DetectorResult.tsx |
Single detector row: name + human/AI pill |
| DetectorScorecard | components/partials/humanizer/DetectorScorecard.tsx |
Grid of all 9 detector results |
| DetectorDynamicMessage | components/partials/humanizer/DetectorDynamicMessage.tsx |
Summary message based on aggregate score |
| DetectorCircleChart | components/partials/detector/DetectorCircleChart.tsx |
Visual score gauge |
From types/index.ts:
export interface StatusProps {
is_scored: string | boolean | null
total: string | boolean | null
gptZero: string | boolean | null
copyLeaks: string | boolean | null
zeroGPT: string | boolean | null
crossPlag: string | boolean | null
sapling: string | boolean | null
writer: string | boolean | null
contScale: string | boolean | null
originality: string | boolean | null
turnitin: string | boolean | null
}
Important: Score values are 0-1 decimals (NOT 0-100). The frontend displays them as percentages: (score * 100).toFixed(0)%. The type is string | boolean | null because false or null indicates the detector did not return a score for that request.
From DetectorResult.tsx:
interface AIDetectorResultProps {
name: string // Display name (e.g., "GPTzero")
isHuman: boolean // true = passed as human, false = flagged as AI
delay: number // Animation stagger delay
isLoading: boolean // Show pulse skeleton
isInactive: boolean // Detector not checked (shows "Not checked" pill)
}
| Detector | Score Field | Type |
|---|---|---|
| GPTzero | gptZero |
Academic AI detection |
| ZeroGPT | zeroGPT |
General-purpose detection |
| Sapling | sapling |
AI content analysis |
| Copyleaks | copyLeaks |
Plagiarism + AI detection |
| Writer | writer |
Content authenticity |
| Turnitin | turnitin |
Academic integrity |
| Originality | originality |
AI + plagiarism detection |
| Crossplag | crossPlag |
Cross-language detection |
| Content at Scale | contScale |
SEO-focused detection |
The detector is called via the detector() function from app/api/auth/register.ts:
// From app/api/auth/register.ts
export const detector = async ({ data, token }) => {
const response = await axios({
url: `${process.env.NEXT_PUBLIC_BACKEND_URL}/api/feature/detector/`,
method: 'post',
data: data,
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
})
return response
}
Built in DetectorButton.tsx:
const userData = {
history_id: historyId, // Document history ID (string | null)
text: safeInputText, // Sanitized input text
content: safeInputText, // Same as text (both fields required)
output_text: safeResult || '', // Humanized output (empty string if none)
is_input: true, // Always true when scanning original input
}
response.data.result // string — sensitivity/classification result
response.data.items // array — per-detector results (stored in setAiItems)
response.data.ai_score // number — aggregate AI score (0-1)
response.data.credit // number — updated credit balance after deduction
From DetectorButton.tsx, the following checks happen before the API call:
| Check | Condition | Action |
|---|---|---|
| Per-request word limit | wordCount > totalWordsPerRequest |
Shows plan limit warning |
| Total credit balance | wordCount > userCredit |
Shows insufficient credits warning |
| Minimum word count | wordCount <= 50 |
Toast: "Please enter at least 50 words" |
| Concurrent requests | Any loading state active | Silently returns |
| Subscription paused | 403 + "subscription is paused" | Toast error |
| Plan content limit | 403 + "content exceeds limit" | Shows plan limit warning |
| Insufficient credits | 403 + "not enough credits" | Shows credit warning |
Credit balance is calculated as: session.user.credit + session.user.referral.credits
Errors from the detector follow two paths:
Axios errors (network/HTTP): Extracted from err.response.data with fallback chain:
const message = err.response?.data?.message
|| err.response?.data?.error
|| err.response?.data?.detail
|| t('aiDetectionFailed')
Non-Axios errors: Generic "An error occurred" toast.
All errors are logged to Sentry via logError() with component tag DetectorButton and user context (userId, email, credits, wordCount, plan).
After any error, the session is refreshed via update({ ...session }) to sync the latest credit balance.
DetectorButton click
│
├── setOriginalDetectorLoading(true)
├── setType('scan')
├── update session (get fresh token)
│
├── [API call]
│
├── On success:
│ ├── setSensitivity(response.data.result)
│ ├── setAiItems(response.data.items) // via HumanizerContext
│ ├── setDetectorScore(response.data.ai_score) // via HumanizerContext
│ ├── update session with new credit balance
│ ├── Scroll to scorecard
│ └── Show info toast
│
└── On error:
├── Show error toast / plan limit UI
├── Log to Sentry
└── Refresh session
| Date | Author | Change |
|---|---|---|
| 2026-01-30 | Admin | Initial creation |
| 2026-01-30 | Docs team | Rewritten with real code: StatusProps interface, API payload, validation rules, error handling, state flow |
Prev: Web App - Humanizer Feature | Next: Web App - Document Management | Up: WalterWrites