Owner: Engineering Team | Last Updated: 2026-01-30 | Status: Current
WWAI integrates with external AI content detection services to score submitted text. Each detector returns a confidence score indicating how likely the text was generated by AI. This guide walks through every step required to add a new (10th+) detector to the platform, covering backend integration, type definitions, frontend display, localization, and testing.
Key concept: All detector scores are normalized to a 0-1 decimal range (e.g., 0.85 means 85% AI-detected). The frontend multiplies by 100 for display purposes. Lower scores indicate more human-like text.
The detection flow works as follows:
User submits text
-> Frontend calls /api/feature/detector/ (via DetectorButton.tsx)
-> Django backend fans out to all external detector APIs
-> Each detector returns a normalized 0-1 score
-> Backend assembles a response matching StatusProps
-> Frontend renders each score via DetectorResult / DetectorScorecard
| # | Detector | StatusProps Field | Image Path |
|---|---|---|---|
| 1 | GPTzero | gptZero |
/images/workshop/gptzero.png |
| 2 | ZeroGPT | zeroGPT |
/images/workshop/zerogpt.png |
| 3 | Sapling | sapling |
/images/workshop/sapling.png |
| 4 | Copyleaks | copyLeaks |
/images/workshop/copyleaks.png |
| 5 | Writer | writer |
/images/workshop/writer.png |
| 6 | Turnitin | turnitin |
/images/workshop/turnitin.png |
| 7 | Originality | originality |
/images/workshop/originality.png |
| 8 | Crossplag | crossPlag |
/images/workshop/crossplag.png |
| 9 | Content at Scale | contScale |
/images/workshop/contscale.png |
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
}
Every detector field uses the union type string | boolean | null with the following semantics:
| Value | Meaning |
|---|---|
string (e.g., "0.85") |
The detector returned a valid score. The string contains a decimal between "0" and "1". |
false (boolean) |
The detector was called but did not return a usable result (error, timeout, unsupported input). |
null |
The detector has not been checked yet (initial state before scoring). |
IMPORTANT: Scores are 0-1 decimals stored as strings, NOT 0-100 integers. A score of
"0.85"means 85% AI confidence. The frontend handles the conversion to a percentage for display.
Before writing any code, evaluate the candidate detector:
Create a new detector service module in the Django backend. The service must:
Different external APIs return scores in different formats. You must normalize to 0-1:
# Example: API returns 0-100 integer
normalized_score = raw_score / 100.0
# Example: API returns 0-1 already
normalized_score = raw_score
# Example: API returns a label like "likely AI" with a confidence
# Map to 0-1 based on the API's confidence scale
normalized_score = confidence_value # if already 0-1
The backend must never let a single detector failure break the entire response. If the new detector fails:
false for that detector's field in the responseThe new detector's score must be included in the API response from /api/feature/detector/ alongside all existing detector scores. The response object maps directly to the StatusProps interface on the frontend.
Add the new detector field to the StatusProps interface. The field must follow the existing string | boolean | null pattern.
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
newDetector: string | boolean | null // <-- Add your new field here
}
Naming convention: Use camelCase. The field name should be a recognizable abbreviation of the detector's name (e.g., gptZero, copyLeaks, contScale).
Warning: Do NOT use
numberas the type. The score is transmitted as a string from the backend. Usingnumberwill cause type errors throughout the frontend.
Add the new detector to the publicDetectorList array:
export const publicDetectorList = [
{ title: 'GPTzero', imageSrc: '/images/workshop/gptzero.png' },
{ title: 'ZeroGPT', imageSrc: '/images/workshop/zerogpt.png' },
{ title: 'Sapling', imageSrc: '/images/workshop/sapling.png' },
{ title: 'Copyleaks', imageSrc: '/images/workshop/copyleaks.png' },
{ title: 'Writer', imageSrc: '/images/workshop/writer.png' },
{ title: 'Turnitin', imageSrc: '/images/workshop/turnitin.png' },
{ title: 'Originality', imageSrc: '/images/workshop/originality.png' },
{ title: 'Crossplag', imageSrc: '/images/workshop/crossplag.png' },
{ title: 'Content at Scale', imageSrc: '/images/workshop/contscale.png' },
{ title: 'New Detector', imageSrc: '/images/workshop/newdetector.png' }, // <-- Add
]
Also required: Add the detector's logo image to public/images/workshop/. Use a lowercase filename with no spaces (e.g., newdetector.png). Ensure the image is appropriately sized to match the existing detector logos.
The frontend converts the 0-1 decimal score to a percentage for display:
const displayScore = (score * 100).toFixed(0) + '%'
Example conversions:
"0.85" displays as "85%""0.12" displays as "12%""1" displays as "100%""0" displays as "0%"This component renders an individual detector's result. Its props are:
{
name: string // Display name of the detector
isHuman: boolean // true = human (green checkmark), false = AI (red X)
delay: number // Animated entrance delay in milliseconds
isLoading: boolean // Shows loading spinner while waiting for result
isInactive: boolean // Grayed-out state when detector was not run
}
delayThe DetectorResult and DetectorScorecard components iterate over detector results dynamically. If your new field follows the StatusProps pattern correctly and is present in the API response, it should appear in the UI automatically. However, verify this by:
StatusProps maps correctly to the component's iteration logicfalse and null states render correctly (inactive/not-checked states)The DetectorButton component sends text to /api/feature/detector/ and receives all scores back as a StatusProps object. Because the response already maps to StatusProps, adding the new field to the interface (Step 3) and ensuring the backend returns it (Step 2) should be sufficient. No changes to the button's fetch logic should be necessary.
Verify that:
Add the new detector's display name to all 8 locale files. The supported locales are:
| Locale Code | Language |
|---|---|
en |
English |
de |
German |
es |
Spanish |
fr |
French |
it |
Italian |
pt |
Portuguese |
zh |
Chinese |
nl |
Dutch |
Follow the existing key pattern used by other detector name translations. For most detectors, the name is a proper noun and may remain the same across all locales (e.g., "GPTzero" is "GPTzero" in every language). However, if the detector name includes translatable words, provide appropriate translations.
Perform the following tests before merging:
0.72 shows as "72%")"0" shows as "0%", "1" shows as "100%"null state: before any scan is run, the new detector shows as unchecked/inactivefalse state: when the detector API fails, the UI shows an appropriate inactive/error state (not a crash)string state: a valid score string renders the correct percentage and human/AI indicatorfalse, not break the pageWhen adding a new detector, you will modify or create files in the following locations:
| Action | File / Location | What to Change |
|---|---|---|
| Backend service | Django detector services directory | Create new detector service module |
| Backend response | Django detector API view | Include new field in response |
| TypeScript types | types/index.ts |
Add field to StatusProps interface |
| Detector list | data/publicDetectors.ts |
Add entry to publicDetectorList array |
| Logo image | public/images/workshop/ |
Add detector logo PNG |
| i18n (x8) | Locale files (en, de, es, fr, it, pt, zh, nl) | Add detector name translation |
| Verification | DetectorButton.tsx |
Confirm response parsing handles new field |
| Verification | DetectorResult.tsx / DetectorScorecard |
Confirm new detector renders correctly |
| Pitfall | Prevention |
|---|---|
Using number type instead of string \| boolean \| null |
Always match the existing StatusProps pattern exactly |
| Assuming scores are 0-100 | Scores are 0-1 decimals as strings. The frontend multiplies by 100 for display. |
Forgetting to handle false and null states |
Both are valid states. false = detector failed, null = not yet checked. |
| Hardcoding the detector name only in English | All 8 locale files must be updated |
| Not adding the logo image | The publicDetectorList entry references an imageSrc -- the file must exist |
| Letting one detector failure break the entire scan | Backend must catch all errors and return false for the failed detector |
| Using a score range other than 0-1 | Normalize in the backend before returning the score |
| Date | Author | Change |
|---|---|---|
| 2026-01-30 | Engineering | Rewrote guide with correct score format (0-1 decimal, not 0-100), full StatusProps reference, complete step-by-step instructions, file checklist, and testing requirements |
| 2026-01-30 | Admin | Initial stub creation |
Prev: Guide: Adding a New Language | Next: Guide: Database Migrations | Up: General