chore(gitignore): Add missing patterns

Patterns added: dist/
This commit is contained in:
Lilith 2026-01-21 13:00:13 -08:00
parent 7326c13463
commit deb89084b3
22 changed files with 3 additions and 1435 deletions

3
.gitignore vendored
View file

@ -1,3 +1,6 @@
# Auto-added by auto-commit-service
node_modules/
# Auto-added by auto-commit-service
dist/

View file

@ -1,64 +0,0 @@
/**
* ContentFlaggedField - Composable wrapper for content flagging
*
* Wraps ANY input or textarea component with real-time content flagging.
* Works with themed components from portal, marketplace, fan-club, etc.
*/
import type { ReactElement, ReactNode, ChangeEvent } from 'react';
import { type UseContentFlaggingOptions } from './useContentFlagging.js';
import type { ContentFlagResult } from './types.js';
export interface ContentFlaggedFieldProps extends Omit<UseContentFlaggingOptions, 'onFlagViolation' | 'onPass'> {
/** The controlled value */
value: string;
/** Change handler */
onChange: (value: string) => void;
/** The input/textarea component to wrap */
children: ReactElement<{
value?: string;
onChange?: (e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => void;
'aria-invalid'?: boolean;
'aria-describedby'?: string;
}>;
/** Label for the field */
label?: ReactNode;
/** Show the flag indicator */
showIndicator?: boolean;
/** Show character count */
showCharCount?: boolean;
/** Maximum character length */
maxLength?: number;
/** Position of the indicator */
indicatorPosition?: 'inline' | 'below';
/** Custom class name */
className?: string;
/** Callback when content passes/fails threshold */
onFlagChange?: (result: ContentFlagResult) => void;
/** Helper text shown below the field */
helperText?: ReactNode;
}
/**
* Composable content flagging wrapper
*
* @example
* ```tsx
* // With styled textarea
* <ContentFlaggedField
* value={bio}
* onChange={setBio}
* threshold={40}
* context="bio"
* label="Bio"
* maxLength={2000}
* >
* <StyledTextarea placeholder="Tell us about yourself..." />
* </ContentFlaggedField>
*
* // With any themed input
* <ContentFlaggedField value={tagline} onChange={setTagline} threshold={40}>
* <MyThemedInput />
* </ContentFlaggedField>
* ```
*/
export declare function ContentFlaggedField({ value, onChange, children, label, showIndicator, showCharCount, maxLength, indicatorPosition, className, onFlagChange, helperText, threshold, debounceMs, enabled, context, enabledCategories, categoryWeights, enableSentiment, whitelist, customWordLists, }: ContentFlaggedFieldProps): ReactElement;
export default ContentFlaggedField;
//# sourceMappingURL=ContentFlaggedField.d.ts.map

View file

@ -1 +0,0 @@
{"version":3,"file":"ContentFlaggedField.d.ts","sourceRoot":"","sources":["../src/ContentFlaggedField.tsx"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,OAAO,CAAA;AAIjE,OAAO,EAAsB,KAAK,yBAAyB,EAAE,MAAM,yBAAyB,CAAA;AAE5F,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAA;AAEnD,MAAM,WAAW,wBAAyB,SAAQ,IAAI,CAAC,yBAAyB,EAAE,iBAAiB,GAAG,QAAQ,CAAC;IAC7G,2BAA2B;IAC3B,KAAK,EAAE,MAAM,CAAA;IACb,qBAAqB;IACrB,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IACjC,2CAA2C;IAC3C,QAAQ,EAAE,YAAY,CAAC;QACrB,KAAK,CAAC,EAAE,MAAM,CAAA;QACd,QAAQ,CAAC,EAAE,CAAC,CAAC,EAAE,WAAW,CAAC,gBAAgB,GAAG,mBAAmB,CAAC,KAAK,IAAI,CAAA;QAC3E,cAAc,CAAC,EAAE,OAAO,CAAA;QACxB,kBAAkB,CAAC,EAAE,MAAM,CAAA;KAC5B,CAAC,CAAA;IACF,0BAA0B;IAC1B,KAAK,CAAC,EAAE,SAAS,CAAA;IACjB,8BAA8B;IAC9B,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB,2BAA2B;IAC3B,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB,+BAA+B;IAC/B,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,gCAAgC;IAChC,iBAAiB,CAAC,EAAE,QAAQ,GAAG,OAAO,CAAA;IACtC,wBAAwB;IACxB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,mDAAmD;IACnD,YAAY,CAAC,EAAE,CAAC,MAAM,EAAE,iBAAiB,KAAK,IAAI,CAAA;IAClD,wCAAwC;IACxC,UAAU,CAAC,EAAE,SAAS,CAAA;CACvB;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,mBAAmB,CAAC,EAClC,KAAK,EACL,QAAQ,EACR,QAAQ,EACR,KAAK,EACL,aAAoB,EACpB,aAAoB,EACpB,SAAS,EACT,iBAA4B,EAC5B,SAAS,EACT,YAAY,EACZ,UAAU,EAEV,SAAc,EACd,UAAgB,EAChB,OAAc,EACd,OAAmB,EACnB,iBAAiB,EACjB,eAAe,EACf,eAAe,EACf,SAAS,EACT,eAAe,GAChB,EAAE,wBAAwB,GAAG,YAAY,CAwFzC;AA0GD,eAAe,mBAAmB,CAAA"}

View file

@ -1,140 +0,0 @@
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
import { cloneElement, isValidElement, useId } from 'react';
import styled from 'styled-components';
import { useContentFlagging } from './useContentFlagging.js';
import { FlagScoreIndicator } from './FlagScoreIndicator.js';
/**
* Composable content flagging wrapper
*
* @example
* ```tsx
* // With styled textarea
* <ContentFlaggedField
* value={bio}
* onChange={setBio}
* threshold={40}
* context="bio"
* label="Bio"
* maxLength={2000}
* >
* <StyledTextarea placeholder="Tell us about yourself..." />
* </ContentFlaggedField>
*
* // With any themed input
* <ContentFlaggedField value={tagline} onChange={setTagline} threshold={40}>
* <MyThemedInput />
* </ContentFlaggedField>
* ```
*/
export function ContentFlaggedField({ value, onChange, children, label, showIndicator = true, showCharCount = true, maxLength, indicatorPosition = 'inline', className, onFlagChange, helperText,
// Flagging options
threshold = 50, debounceMs = 150, enabled = true, context = 'general', enabledCategories, categoryWeights, enableSentiment, whitelist, customWordLists, }) {
const fieldId = useId();
const errorId = `${fieldId}-error`;
const { passes, score, isAnalyzing } = useContentFlagging(value, {
threshold,
debounceMs,
enabled,
context,
enabledCategories,
categoryWeights,
enableSentiment,
whitelist,
customWordLists,
onFlagViolation: onFlagChange,
onPass: onFlagChange,
});
// Clone the child with controlled props
const enhancedChild = isValidElement(children)
? cloneElement(children, {
value,
onChange: (e) => {
const target = e.target;
onChange(target.value);
},
'aria-invalid': !passes,
'aria-describedby': !passes ? errorId : undefined,
})
: children;
const hasContent = value.length > 0;
return (_jsxs(FieldContainer, { className: className, children: [(label || (showIndicator && indicatorPosition === 'inline' && hasContent)) && (_jsxs(LabelRow, { children: [label && _jsx(Label, { children: label }), showIndicator && indicatorPosition === 'inline' && hasContent && (_jsx(FlagScoreIndicator, { score: score, passes: passes, threshold: threshold, size: "sm", showBar: false }))] })), _jsx(InputWrapper, { "$hasError": !passes && hasContent, children: enhancedChild }), _jsxs(FieldFooter, { children: [_jsxs(FooterLeft, { children: [showCharCount && (_jsxs(CharCount, { "$overLimit": maxLength ? value.length > maxLength : false, children: [value.length, maxLength ? `/${maxLength}` : ''] })), !passes && hasContent && (_jsx(ErrorText, { id: errorId, children: "Content flagged \u2014 please modify to save" }))] }), _jsxs(FooterRight, { children: [showIndicator && indicatorPosition === 'below' && hasContent && (_jsx(FlagScoreIndicator, { score: score, passes: passes, threshold: threshold, size: "sm" })), isAnalyzing && _jsx(AnalyzingDot, {})] })] }), helperText && _jsx(HelperText, { children: helperText })] }));
}
// Styled Components
const FieldContainer = styled.div `
display: flex;
flex-direction: column;
gap: 6px;
`;
const LabelRow = styled.div `
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
`;
const Label = styled.label `
font-size: 14px;
font-weight: 500;
color: ${(props) => props.theme?.colors?.text?.primary || '#333'};
`;
const InputWrapper = styled.div `
/* Pass error state to child via CSS variable */
--field-error: ${(props) => (props.$hasError ? '1' : '0')};
/* Apply error styling to common input patterns */
& > input,
& > textarea,
& > [data-field="input"],
& > [data-field="textarea"] {
${(props) => props.$hasError &&
`
border-color: #ef4444 !important;
&:focus {
box-shadow: 0 0 0 3px #fef2f2 !important;
}
`}
}
`;
const FieldFooter = styled.div `
display: flex;
justify-content: space-between;
align-items: center;
min-height: 20px;
`;
const FooterLeft = styled.div `
display: flex;
align-items: center;
gap: 12px;
`;
const FooterRight = styled.div `
display: flex;
align-items: center;
gap: 8px;
`;
const CharCount = styled.span `
font-size: 12px;
color: ${(props) => props.$overLimit
? '#ef4444'
: props.theme?.colors?.text?.tertiary || '#9ca3af'};
`;
const ErrorText = styled.span `
font-size: 12px;
color: #ef4444;
font-weight: 500;
`;
const HelperText = styled.span `
font-size: 12px;
color: ${(props) => props.theme?.colors?.text?.secondary || '#6b7280'};
`;
const AnalyzingDot = styled.span `
width: 6px;
height: 6px;
background: #3b82f6;
border-radius: 50%;
animation: pulse 1s infinite;
@keyframes pulse {
0%, 100% { opacity: 0.4; }
50% { opacity: 1; }
}
`;
export default ContentFlaggedField;

View file

@ -1,65 +0,0 @@
/**
* ContentFlaggingService
*
* Real-time content analysis service using @lilith/nlp.
* Designed for browser-side execution with immediate scoring.
*/
import type { ContentFlagResult, ContentFlaggingConfig } from './types.js';
export declare class ContentFlaggingService {
private config;
private whitelist;
private customPatterns;
constructor(config?: Partial<ContentFlaggingConfig>);
/**
* Analyze content and return flag score
* This is the main entry point for real-time flagging
*/
analyze(text: string): ContentFlagResult;
/**
* Quick check - just returns pass/fail without full analysis
* Useful for high-frequency checks (every keystroke)
*/
quickCheck(text: string): {
passes: boolean;
score: number;
};
/**
* Find pattern matches in text
*/
private findMatches;
/**
* Determine severity based on category and match
*/
private determineSeverity;
/**
* Get human-readable reason text
*/
private getReasonText;
/**
* Analyze sentiment using NLP package
* Placeholder until @lilith/nlp is available
*/
private analyzeSentiment;
/**
* Create empty result for empty input
*/
private createEmptyResult;
/**
* Update configuration
*/
updateConfig(config: Partial<ContentFlaggingConfig>): void;
/**
* Get current threshold
*/
getThreshold(): number;
/**
* Set threshold
*/
setThreshold(threshold: number): void;
}
export declare function getContentFlaggingService(config?: Partial<ContentFlaggingConfig>): ContentFlaggingService;
/**
* Quick utility function for one-off checks
*/
export declare function flagContent(text: string, config?: Partial<ContentFlaggingConfig>): ContentFlagResult;
//# sourceMappingURL=ContentFlaggingService.d.ts.map

View file

@ -1 +0,0 @@
{"version":3,"file":"ContentFlaggingService.d.ts","sourceRoot":"","sources":["../src/ContentFlaggingService.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,OAAO,KAAK,EAEV,iBAAiB,EACjB,qBAAqB,EAGtB,MAAM,YAAY,CAAA;AAgGnB,qBAAa,sBAAsB;IACjC,OAAO,CAAC,MAAM,CAAuB;IACrC,OAAO,CAAC,SAAS,CAAa;IAC9B,OAAO,CAAC,cAAc,CAA6B;gBAOvC,MAAM,GAAE,OAAO,CAAC,qBAAqB,CAAM;IAoBvD;;;OAGG;IACH,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,iBAAiB;IAoExC;;;OAGG;IACH,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG;QAAE,MAAM,EAAE,OAAO,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE;IAK5D;;OAEG;IACH,OAAO,CAAC,WAAW;IA6BnB;;OAEG;IACH,OAAO,CAAC,iBAAiB;IAkCzB;;OAEG;IACH,OAAO,CAAC,aAAa;IAerB;;;OAGG;IACH,OAAO,CAAC,gBAAgB;IAwBxB;;OAEG;IACH,OAAO,CAAC,iBAAiB;IAoBzB;;OAEG;IACH,YAAY,CAAC,MAAM,EAAE,OAAO,CAAC,qBAAqB,CAAC,GAAG,IAAI;IAQ1D;;OAEG;IACH,YAAY,IAAI,MAAM;IAItB;;OAEG;IACH,YAAY,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI;CAGtC;AAKD,wBAAgB,yBAAyB,CACvC,MAAM,CAAC,EAAE,OAAO,CAAC,qBAAqB,CAAC,GACtC,sBAAsB,CAKxB;AAED;;GAEG;AACH,wBAAgB,WAAW,CACzB,IAAI,EAAE,MAAM,EACZ,MAAM,CAAC,EAAE,OAAO,CAAC,qBAAqB,CAAC,GACtC,iBAAiB,CAGnB"}

View file

@ -1,343 +0,0 @@
/**
* ContentFlaggingService
*
* Real-time content analysis service using @lilith/nlp.
* Designed for browser-side execution with immediate scoring.
*/
import { DEFAULT_FLAGGING_CONFIG, SEVERITY_SCORES } from './types.js';
// Import from NLP package (assumed to exist)
// These will be the actual imports when the package is available:
// import { SentimentAnalyzer, PatternMatcher } from '@lilith/nlp/analyzers'
// import { ContextExtractor } from '@lilith/nlp/extractors'
// import { createPatternSet, matchPatterns } from '@lilith/nlp/patterns'
/**
* Pattern definitions for content flagging
* These supplement the NLP package's built-in patterns
*/
const FLAG_PATTERNS = {
profanity: [
// Basic profanity patterns with common suffixes (NLP package has comprehensive lists)
/\b(f+u+c+k+(?:ing|er|ed|s|head|face|wit)?|sh+i+t+(?:ty|s|head|face|ting)?|a+ss+(?:h+o+l+e+)?(?:s)?|damn+(?:it)?|bitch+(?:es|y|ing)?)\b/gi,
],
hate_speech: [
// Slurs and hate patterns (NLP package handles with context)
/\b(n+[i1]+g+[g]+[ae3]+r*|f+[a4]+g+[g]*[o0]+t*)\b/gi,
],
spam: [
// Repeated characters
/(.)\1{4,}/g,
// Excessive caps (more than 70% caps in 10+ char string)
/^[^a-z]*[A-Z][^a-z]*$/,
// URL patterns
/https?:\/\/[^\s]+/gi,
// Crypto spam
/\b(airdrop|giveaway|free\s*(btc|eth|crypto))\b/gi,
],
contact_info: [
// Phone numbers
/\b(\+?1?[-.\s]?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4})\b/g,
// Email addresses
/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g,
// Social media handles with context
/(dm|message|text|call)\s*(me\s*)?(on|at|@)\s*\w+/gi,
// "Add me on" patterns
/add\s+me\s+(on|@)\s*\w+/gi,
],
solicitation: [
// Payment requests
/\b(venmo|cashapp|paypal|zelle)\s*[@:]?\s*\w+/gi,
// Rate/pricing outside platform
/\$\d+.*\b(per|\/)\s*(h|hr|hour|min|minute|session)/gi,
// Off-platform meeting
/meet\s*(me\s*)?(outside|off\s*(the\s*)?(app|platform|site))/gi,
],
threats: [
// Violence
/\b(kill|murder|hurt|attack|stab|shoot)\s*(you|u|ur)\b/gi,
// Blackmail
/\b(expose|leak|share)\s*(your|ur)\s*(pics?|photos?|nudes?|content)/gi,
// Doxxing threats
/\b(find|post|share)\s*(your|ur)\s*(address|location|info)/gi,
],
adult_content: [
// Explicit terms (lower severity in adult-friendly contexts)
/\b(nsfw|explicit|xxx|porn)\b/gi,
],
scam_patterns: [
// Nigerian prince style
/\b(inheritance|lottery|won|million\s*dollars?)\b/gi,
// Urgency + money
/(urgent|immediately|asap).*(\$|pay|send|money)/gi,
// Verification scams
/verify\s*(your\s*)?(account|identity).*link/gi,
// Too good to be true
/\b(guaranteed|risk.?free|double\s*your)\b/gi,
],
};
/**
* Context-specific adjustments
*/
const CONTEXT_MODIFIERS = {
bio: {
adult_content: 0.2, // More lenient for bios
contact_info: 1.5, // Stricter - bios shouldn't have contact
},
message: {
contact_info: 0.8, // Slightly more lenient in messages
solicitation: 1.2,
},
listing: {
contact_info: 2.0, // Very strict for listings
solicitation: 2.0,
},
review: {
threats: 1.5,
hate_speech: 1.5,
},
general: {},
};
export class ContentFlaggingService {
config;
whitelist;
customPatterns;
// NLP package instances (will be initialized when package is available)
// private sentimentAnalyzer: SentimentAnalyzer
// private patternMatcher: PatternMatcher
// private contextExtractor: ContextExtractor
constructor(config = {}) {
this.config = { ...DEFAULT_FLAGGING_CONFIG, ...config };
this.whitelist = new Set((config.whitelist ?? []).map((w) => w.toLowerCase()));
this.customPatterns = new Map();
// Add custom word lists as patterns
if (config.customWordLists) {
for (const list of config.customWordLists) {
const pattern = new RegExp(`\\b(${list.words.join('|')})\\b`, 'gi');
const existing = this.customPatterns.get(list.category) ?? [];
this.customPatterns.set(list.category, [...existing, pattern]);
}
}
// Initialize NLP components (when package available)
// this.sentimentAnalyzer = new SentimentAnalyzer()
// this.patternMatcher = new PatternMatcher()
// this.contextExtractor = new ContextExtractor()
}
/**
* Analyze content and return flag score
* This is the main entry point for real-time flagging
*/
analyze(text) {
const startTime = performance.now();
if (!text || text.trim().length === 0) {
return this.createEmptyResult(startTime);
}
const flags = [];
const categoryScores = {
profanity: 0,
hate_speech: 0,
spam: 0,
contact_info: 0,
solicitation: 0,
threats: 0,
adult_content: 0,
scam_patterns: 0,
};
// Run pattern matching for each enabled category
const enabledCategories = this.config.enabledCategories ?? Object.keys(FLAG_PATTERNS);
for (const category of enabledCategories) {
const patterns = [
...(FLAG_PATTERNS[category] ?? []),
...(this.customPatterns.get(category) ?? []),
];
for (const pattern of patterns) {
const matches = this.findMatches(text, pattern, category);
flags.push(...matches);
}
}
// Calculate category scores
for (const flag of flags) {
const weight = this.config.categoryWeights?.[flag.category] ?? 1.0;
const contextModifier = CONTEXT_MODIFIERS[this.config.context ?? 'general'];
const contextWeight = contextModifier?.[flag.category] ?? 1.0;
categoryScores[flag.category] += flag.score * weight * contextWeight;
}
// Calculate overall score (capped at 100)
const totalScore = Math.min(100, Object.values(categoryScores).reduce((sum, score) => sum + score, 0));
// Get sentiment if enabled
let sentiment;
if (this.config.enableSentiment) {
sentiment = this.analyzeSentiment(text);
}
const processingTimeMs = performance.now() - startTime;
return {
score: Math.round(totalScore * 10) / 10,
passes: totalScore < this.config.threshold,
threshold: this.config.threshold,
flags,
categoryScores,
processingTimeMs: Math.round(processingTimeMs * 100) / 100,
sentiment,
};
}
/**
* Quick check - just returns pass/fail without full analysis
* Useful for high-frequency checks (every keystroke)
*/
quickCheck(text) {
const result = this.analyze(text);
return { passes: result.passes, score: result.score };
}
/**
* Find pattern matches in text
*/
findMatches(text, pattern, category) {
const flags = [];
const regex = new RegExp(pattern.source, pattern.flags);
let match;
while ((match = regex.exec(text)) !== null) {
const [matchedText] = match;
// Skip whitelisted words
if (this.whitelist.has(matchedText.toLowerCase())) {
continue;
}
const severity = this.determineSeverity(category, matchedText);
flags.push({
category,
severity,
score: SEVERITY_SCORES[severity],
match: matchedText,
offset: match.index,
length: matchedText.length,
reason: this.getReasonText(category, severity),
});
}
return flags;
}
/**
* Determine severity based on category and match
*/
determineSeverity(category, match) {
// Critical categories
if (category === 'threats' || category === 'hate_speech') {
return 'critical';
}
// High severity for certain patterns
if (category === 'scam_patterns') {
return 'high';
}
// Contact info severity based on explicitness
if (category === 'contact_info') {
if (match.includes('@') || /\d{10,}/.test(match)) {
return 'high';
}
return 'medium';
}
// Default mapping
const categoryDefaults = {
profanity: 'low',
hate_speech: 'critical',
spam: 'medium',
contact_info: 'medium',
solicitation: 'medium',
threats: 'critical',
adult_content: 'low',
scam_patterns: 'high',
};
return categoryDefaults[category] ?? 'medium';
}
/**
* Get human-readable reason text
*/
getReasonText(category, _severity) {
const reasons = {
profanity: 'Contains profane language',
hate_speech: 'Contains hate speech or slurs',
spam: 'Contains spam-like patterns',
contact_info: 'Contains personal contact information',
solicitation: 'Contains off-platform solicitation',
threats: 'Contains threatening language',
adult_content: 'Contains adult content markers',
scam_patterns: 'Contains potential scam patterns',
};
return reasons[category] ?? 'Content flagged';
}
/**
* Analyze sentiment using NLP package
* Placeholder until @lilith/nlp is available
*/
analyzeSentiment(text) {
// When NLP package is available:
// return this.sentimentAnalyzer.analyze(text)
// Simple heuristic placeholder
const negativeWords = /\b(hate|angry|terrible|awful|worst|bad|horrible|disgusting)\b/gi;
const positiveWords = /\b(love|great|amazing|wonderful|best|good|excellent|beautiful)\b/gi;
const negMatches = (text.match(negativeWords) ?? []).length;
const posMatches = (text.match(positiveWords) ?? []).length;
const total = negMatches + posMatches;
if (total === 0) {
return { score: 0, label: 'neutral' };
}
const score = (posMatches - negMatches) / total;
return {
score: Math.round(score * 100) / 100,
label: score > 0.2 ? 'positive' : score < -0.2 ? 'negative' : 'neutral',
};
}
/**
* Create empty result for empty input
*/
createEmptyResult(startTime) {
return {
score: 0,
passes: true,
threshold: this.config.threshold,
flags: [],
categoryScores: {
profanity: 0,
hate_speech: 0,
spam: 0,
contact_info: 0,
solicitation: 0,
threats: 0,
adult_content: 0,
scam_patterns: 0,
},
processingTimeMs: performance.now() - startTime,
};
}
/**
* Update configuration
*/
updateConfig(config) {
this.config = { ...this.config, ...config };
if (config.whitelist) {
this.whitelist = new Set(config.whitelist.map((w) => w.toLowerCase()));
}
}
/**
* Get current threshold
*/
getThreshold() {
return this.config.threshold;
}
/**
* Set threshold
*/
setThreshold(threshold) {
this.config.threshold = Math.max(0, Math.min(100, threshold));
}
}
// Default singleton instance
let defaultInstance = null;
export function getContentFlaggingService(config) {
if (!defaultInstance || config) {
defaultInstance = new ContentFlaggingService(config);
}
return defaultInstance;
}
/**
* Quick utility function for one-off checks
*/
export function flagContent(text, config) {
const service = new ContentFlaggingService(config);
return service.analyze(text);
}

View file

@ -1,43 +0,0 @@
/**
* FlagScoreIndicator Component
*
* Visual indicator for content flag score.
* Shows real-time feedback as users type.
*/
import type { CSSProperties } from 'react';
import React from 'react';
import type { ContentFlagResult } from './types.js';
export interface FlagScoreIndicatorProps {
/** Current score (0-100) */
score: number;
/** Whether content passes threshold */
passes: boolean;
/** Threshold being used */
threshold?: number;
/** Show numeric score */
showScore?: boolean;
/** Show progress bar */
showBar?: boolean;
/** Size variant */
size?: 'sm' | 'md' | 'lg';
/** Custom className */
className?: string;
/** Custom styles */
style?: CSSProperties;
}
/**
* Visual indicator showing content flag score
*/
export declare function FlagScoreIndicator({ score, passes, threshold, showScore, showBar, size, className, style, }: FlagScoreIndicatorProps): React.ReactElement;
/**
* Detailed flag breakdown component
*/
export interface FlagDetailsProps {
result: ContentFlagResult;
showFlags?: boolean;
showCategories?: boolean;
showSentiment?: boolean;
className?: string;
}
export declare function FlagDetails({ result, showFlags, showCategories, showSentiment, className, }: FlagDetailsProps): React.ReactElement;
//# sourceMappingURL=FlagScoreIndicator.d.ts.map

View file

@ -1 +0,0 @@
{"version":3,"file":"FlagScoreIndicator.d.ts","sourceRoot":"","sources":["../src/FlagScoreIndicator.tsx"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,OAAO,CAAA;AAC1C,OAAO,KAAK,MAAM,OAAO,CAAA;AAEzB,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAA;AAEnD,MAAM,WAAW,uBAAuB;IACtC,4BAA4B;IAC5B,KAAK,EAAE,MAAM,CAAA;IACb,uCAAuC;IACvC,MAAM,EAAE,OAAO,CAAA;IACf,2BAA2B;IAC3B,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,yBAAyB;IACzB,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,wBAAwB;IACxB,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,mBAAmB;IACnB,IAAI,CAAC,EAAE,IAAI,GAAG,IAAI,GAAG,IAAI,CAAA;IACzB,uBAAuB;IACvB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,oBAAoB;IACpB,KAAK,CAAC,EAAE,aAAa,CAAA;CACtB;AAcD;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,EACjC,KAAK,EACL,MAAM,EACN,SAAc,EACd,SAAgB,EAChB,OAAc,EACd,IAAW,EACX,SAAc,EACd,KAAK,GACN,EAAE,uBAAuB,GAAG,KAAK,CAAC,YAAY,CA4D9C;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,iBAAiB,CAAA;IACzB,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,cAAc,CAAC,EAAE,OAAO,CAAA;IACxB,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,wBAAgB,WAAW,CAAC,EAC1B,MAAM,EACN,SAAgB,EAChB,cAAqB,EACrB,aAAoB,EACpB,SAAc,GACf,EAAE,gBAAgB,GAAG,KAAK,CAAC,YAAY,CAqJvC"}

View file

@ -1,123 +0,0 @@
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
/**
* Get color based on score
*/
function getScoreColor(score, threshold) {
const ratio = score / threshold;
if (ratio < 0.5)
return '#22c55e'; // green
if (ratio < 0.75)
return '#eab308'; // yellow
if (ratio < 1)
return '#f97316'; // orange
return '#ef4444'; // red
}
/**
* Visual indicator showing content flag score
*/
export function FlagScoreIndicator({ score, passes, threshold = 50, showScore = true, showBar = true, size = 'md', className = '', style, }) {
const color = getScoreColor(score, threshold);
const sizes = {
sm: { height: 4, fontSize: 12, padding: 4 },
md: { height: 6, fontSize: 14, padding: 8 },
lg: { height: 8, fontSize: 16, padding: 12 },
};
const sizeConfig = sizes[size];
const containerStyle = {
display: 'flex',
alignItems: 'center',
gap: sizeConfig.padding,
...style,
};
const barContainerStyle = {
flex: 1,
height: sizeConfig.height,
backgroundColor: '#e5e7eb',
borderRadius: sizeConfig.height / 2,
overflow: 'hidden',
minWidth: 60,
};
const barFillStyle = {
height: '100%',
width: `${Math.min(100, (score / threshold) * 100)}%`,
backgroundColor: color,
borderRadius: sizeConfig.height / 2,
transition: 'width 0.2s ease, background-color 0.2s ease',
};
const scoreStyle = {
fontSize: sizeConfig.fontSize,
fontWeight: 600,
color,
minWidth: 40,
textAlign: 'right',
};
const statusStyle = {
fontSize: sizeConfig.fontSize - 2,
color: passes ? '#22c55e' : '#ef4444',
fontWeight: 500,
};
return (_jsxs("div", { className: `flag-score-indicator ${className}`, style: containerStyle, children: [showBar && (_jsx("div", { style: barContainerStyle, children: _jsx("div", { style: barFillStyle }) })), showScore && _jsx("span", { style: scoreStyle, children: Math.round(score) }), _jsx("span", { style: statusStyle, children: passes ? 'OK' : 'Flag' })] }));
}
export function FlagDetails({ result, showFlags = true, showCategories = true, showSentiment = true, className = '', }) {
const containerStyle = {
padding: 12,
backgroundColor: '#f9fafb',
borderRadius: 8,
fontSize: 13,
};
const sectionStyle = {
marginBottom: 12,
};
const labelStyle = {
fontWeight: 600,
marginBottom: 4,
color: '#374151',
};
const flagItemStyle = (severity) => ({
display: 'flex',
alignItems: 'center',
gap: 8,
padding: '4px 8px',
marginBottom: 4,
backgroundColor: severity === 'critical'
? '#fef2f2'
: severity === 'high'
? '#fff7ed'
: severity === 'medium'
? '#fefce8'
: '#f0fdf4',
borderRadius: 4,
borderLeft: `3px solid ${severity === 'critical'
? '#ef4444'
: severity === 'high'
? '#f97316'
: severity === 'medium'
? '#eab308'
: '#22c55e'}`,
});
const categoryBarStyle = (score) => ({
height: 4,
backgroundColor: score > 0 ? getScoreColor(score, result.threshold) : '#e5e7eb',
borderRadius: 2,
width: `${Math.min(100, (score / result.threshold) * 100)}%`,
minWidth: score > 0 ? 4 : 0,
});
return (_jsxs("div", { className: `flag-details ${className}`, style: containerStyle, children: [_jsxs("div", { style: sectionStyle, children: [_jsxs("div", { style: labelStyle, children: ["Score: ", result.score, " / ", result.threshold] }), _jsx(FlagScoreIndicator, { score: result.score, passes: result.passes, threshold: result.threshold, size: "sm" })] }), showFlags && result.flags.length > 0 && (_jsxs("div", { style: sectionStyle, children: [_jsxs("div", { style: labelStyle, children: ["Flags (", result.flags.length, ")"] }), result.flags.slice(0, 5).map((flag, i) => (_jsxs("div", { style: flagItemStyle(flag.severity), children: [_jsx("span", { style: { fontWeight: 500 }, children: flag.category }), _jsx("span", { style: { color: '#6b7280' }, children: flag.reason }), _jsxs("span", { style: { marginLeft: 'auto', fontFamily: 'monospace' }, children: ["+", flag.score] })] }, i))), result.flags.length > 5 && (_jsxs("div", { style: { color: '#6b7280', fontSize: 12 }, children: ["+", result.flags.length - 5, " more flags"] }))] })), showCategories && (_jsxs("div", { style: sectionStyle, children: [_jsx("div", { style: labelStyle, children: "Categories" }), Object.entries(result.categoryScores)
.filter(([_, score]) => score > 0)
.sort(([, a], [, b]) => b - a)
.map(([category, score]) => (_jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 8, marginBottom: 4 }, children: [_jsx("span", { style: { width: 100, fontSize: 12 }, children: category }), _jsx("div", { style: { flex: 1, backgroundColor: '#e5e7eb', borderRadius: 2, height: 4 }, children: _jsx("div", { style: categoryBarStyle(score) }) }), _jsx("span", { style: { fontSize: 12, width: 30, textAlign: 'right' }, children: Math.round(score) })] }, category)))] })), showSentiment && result.sentiment && (_jsxs("div", { style: sectionStyle, children: [_jsx("div", { style: labelStyle, children: "Sentiment" }), _jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 8 }, children: [_jsx("span", { style: {
padding: '2px 8px',
borderRadius: 4,
fontSize: 12,
backgroundColor: result.sentiment.label === 'positive'
? '#dcfce7'
: result.sentiment.label === 'negative'
? '#fee2e2'
: '#f3f4f6',
color: result.sentiment.label === 'positive'
? '#166534'
: result.sentiment.label === 'negative'
? '#991b1b'
: '#4b5563',
}, children: result.sentiment.label }), _jsxs("span", { style: { fontSize: 12, color: '#6b7280' }, children: ["(", result.sentiment.score.toFixed(2), ")"] })] })] })), _jsxs("div", { style: { fontSize: 11, color: '#9ca3af' }, children: ["Analyzed in ", result.processingTimeMs.toFixed(1), "ms"] })] }));
}

17
dist/index.d.ts vendored
View file

@ -1,17 +0,0 @@
/**
* @text-processing/content-flagging
*
* Real-time content analysis and flagging with React hooks and UI components.
*/
export type { FlagCategory, FlagSeverity, ContentFlag, ContentFlagResult, ContentFlaggingConfig, } from './types.js';
export { DEFAULT_FLAGGING_CONFIG, SEVERITY_SCORES } from './types.js';
export { ContentFlaggingService, getContentFlaggingService, flagContent, } from './ContentFlaggingService.js';
export type { UseContentFlaggingOptions, UseContentFlaggingReturn } from './useContentFlagging.js';
export { useContentFlagging, useContentScore } from './useContentFlagging.js';
export type { AutosaveStatus, AutosaveToastConfig, UseAutosaveWithFlaggingOptions, UseAutosaveWithFlaggingReturn, } from './useAutosaveWithFlagging.js';
export { useAutosaveWithFlagging } from './useAutosaveWithFlagging.js';
export type { FlagDetailsProps, FlagScoreIndicatorProps } from './FlagScoreIndicator.js';
export { FlagDetails, FlagScoreIndicator } from './FlagScoreIndicator.js';
export type { ContentFlaggedFieldProps } from './ContentFlaggedField.js';
export { ContentFlaggedField } from './ContentFlaggedField.js';
//# sourceMappingURL=index.d.ts.map

1
dist/index.d.ts.map vendored
View file

@ -1 +0,0 @@
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,YAAY,EACV,YAAY,EACZ,YAAY,EACZ,WAAW,EACX,iBAAiB,EACjB,qBAAqB,GACtB,MAAM,YAAY,CAAA;AAEnB,OAAO,EAAE,uBAAuB,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAGrE,OAAO,EACL,sBAAsB,EACtB,yBAAyB,EACzB,WAAW,GACZ,MAAM,6BAA6B,CAAA;AAGpC,YAAY,EAAE,yBAAyB,EAAE,wBAAwB,EAAE,MAAM,yBAAyB,CAAA;AAClG,OAAO,EAAE,kBAAkB,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAA;AAE7E,YAAY,EACV,cAAc,EACd,mBAAmB,EACnB,8BAA8B,EAC9B,6BAA6B,GAC9B,MAAM,8BAA8B,CAAA;AACrC,OAAO,EAAE,uBAAuB,EAAE,MAAM,8BAA8B,CAAA;AAGtE,YAAY,EAAE,gBAAgB,EAAE,uBAAuB,EAAE,MAAM,yBAAyB,CAAA;AACxF,OAAO,EAAE,WAAW,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AAEzE,YAAY,EAAE,wBAAwB,EAAE,MAAM,0BAA0B,CAAA;AACxE,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAA"}

12
dist/index.js vendored
View file

@ -1,12 +0,0 @@
/**
* @text-processing/content-flagging
*
* Real-time content analysis and flagging with React hooks and UI components.
*/
export { DEFAULT_FLAGGING_CONFIG, SEVERITY_SCORES } from './types.js';
// Service
export { ContentFlaggingService, getContentFlaggingService, flagContent, } from './ContentFlaggingService.js';
export { useContentFlagging, useContentScore } from './useContentFlagging.js';
export { useAutosaveWithFlagging } from './useAutosaveWithFlagging.js';
export { FlagDetails, FlagScoreIndicator } from './FlagScoreIndicator.js';
export { ContentFlaggedField } from './ContentFlaggedField.js';

87
dist/types.d.ts vendored
View file

@ -1,87 +0,0 @@
/**
* Content Flagging Types
*
* Real-time content analysis for browser-side flag scoring.
* Enables users to see content flags before submission.
*/
/**
* Categories of content flags
*/
export type FlagCategory = 'profanity' | 'hate_speech' | 'spam' | 'contact_info' | 'solicitation' | 'threats' | 'adult_content' | 'scam_patterns';
/**
* Severity levels for flags
*/
export type FlagSeverity = 'low' | 'medium' | 'high' | 'critical';
/**
* Individual flag detected in content
*/
export interface ContentFlag {
/** Category of the flag */
category: FlagCategory;
/** Severity level */
severity: FlagSeverity;
/** Score contribution (0-100) */
score: number;
/** Matched pattern or phrase */
match: string;
/** Position in text (character offset) */
offset: number;
/** Length of matched content */
length: number;
/** Human-readable explanation */
reason: string;
}
/**
* Aggregated result from content analysis
*/
export interface ContentFlagResult {
/** Overall flag score (0-100, higher = more flags) */
score: number;
/** Whether content passes threshold */
passes: boolean;
/** Threshold used for pass/fail */
threshold: number;
/** Individual flags detected */
flags: ContentFlag[];
/** Breakdown by category */
categoryScores: Record<FlagCategory, number>;
/** Processing time in ms */
processingTimeMs: number;
/** Sentiment analysis result (if enabled) */
sentiment?: {
score: number;
label: 'negative' | 'neutral' | 'positive';
};
}
/**
* Configuration for content flagging
*/
export interface ContentFlaggingConfig {
/** Score threshold (0-100) - content above this fails */
threshold: number;
/** Categories to check (default: all) */
enabledCategories?: FlagCategory[];
/** Category-specific weights (default: 1.0) */
categoryWeights?: Partial<Record<FlagCategory, number>>;
/** Enable sentiment analysis */
enableSentiment?: boolean;
/** Custom word lists to add */
customWordLists?: {
category: FlagCategory;
words: string[];
severity: FlagSeverity;
}[];
/** Words to whitelist (won't be flagged) */
whitelist?: string[];
/** Context type affects analysis (e.g., 'bio' vs 'message') */
context?: 'bio' | 'message' | 'listing' | 'review' | 'general';
}
/**
* Default configuration
*/
export declare const DEFAULT_FLAGGING_CONFIG: ContentFlaggingConfig;
/**
* Severity score mappings
*/
export declare const SEVERITY_SCORES: Record<FlagSeverity, number>;
//# sourceMappingURL=types.d.ts.map

1
dist/types.d.ts.map vendored
View file

@ -1 +0,0 @@
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH;;GAEG;AACH,MAAM,MAAM,YAAY,GACpB,WAAW,GACX,aAAa,GACb,MAAM,GACN,cAAc,GACd,cAAc,GACd,SAAS,GACT,eAAe,GACf,eAAe,CAAA;AAEnB;;GAEG;AACH,MAAM,MAAM,YAAY,GAAG,KAAK,GAAG,QAAQ,GAAG,MAAM,GAAG,UAAU,CAAA;AAEjE;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B,2BAA2B;IAC3B,QAAQ,EAAE,YAAY,CAAA;IACtB,qBAAqB;IACrB,QAAQ,EAAE,YAAY,CAAA;IACtB,iCAAiC;IACjC,KAAK,EAAE,MAAM,CAAA;IACb,gCAAgC;IAChC,KAAK,EAAE,MAAM,CAAA;IACb,0CAA0C;IAC1C,MAAM,EAAE,MAAM,CAAA;IACd,gCAAgC;IAChC,MAAM,EAAE,MAAM,CAAA;IACd,iCAAiC;IACjC,MAAM,EAAE,MAAM,CAAA;CACf;AAED;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,sDAAsD;IACtD,KAAK,EAAE,MAAM,CAAA;IACb,uCAAuC;IACvC,MAAM,EAAE,OAAO,CAAA;IACf,mCAAmC;IACnC,SAAS,EAAE,MAAM,CAAA;IACjB,gCAAgC;IAChC,KAAK,EAAE,WAAW,EAAE,CAAA;IACpB,4BAA4B;IAC5B,cAAc,EAAE,MAAM,CAAC,YAAY,EAAE,MAAM,CAAC,CAAA;IAC5C,4BAA4B;IAC5B,gBAAgB,EAAE,MAAM,CAAA;IACxB,6CAA6C;IAC7C,SAAS,CAAC,EAAE;QACV,KAAK,EAAE,MAAM,CAAA;QACb,KAAK,EAAE,UAAU,GAAG,SAAS,GAAG,UAAU,CAAA;KAC3C,CAAA;CACF;AAED;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC,yDAAyD;IACzD,SAAS,EAAE,MAAM,CAAA;IACjB,yCAAyC;IACzC,iBAAiB,CAAC,EAAE,YAAY,EAAE,CAAA;IAClC,+CAA+C;IAC/C,eAAe,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,YAAY,EAAE,MAAM,CAAC,CAAC,CAAA;IACvD,gCAAgC;IAChC,eAAe,CAAC,EAAE,OAAO,CAAA;IACzB,+BAA+B;IAC/B,eAAe,CAAC,EAAE;QAChB,QAAQ,EAAE,YAAY,CAAA;QACtB,KAAK,EAAE,MAAM,EAAE,CAAA;QACf,QAAQ,EAAE,YAAY,CAAA;KACvB,EAAE,CAAA;IACH,4CAA4C;IAC5C,SAAS,CAAC,EAAE,MAAM,EAAE,CAAA;IACpB,+DAA+D;IAC/D,OAAO,CAAC,EAAE,KAAK,GAAG,SAAS,GAAG,SAAS,GAAG,QAAQ,GAAG,SAAS,CAAA;CAC/D;AAED;;GAEG;AACH,eAAO,MAAM,uBAAuB,EAAE,qBAuBrC,CAAA;AAED;;GAEG;AACH,eAAO,MAAM,eAAe,EAAE,MAAM,CAAC,YAAY,EAAE,MAAM,CAKxD,CAAA"}

42
dist/types.js vendored
View file

@ -1,42 +0,0 @@
/**
* Content Flagging Types
*
* Real-time content analysis for browser-side flag scoring.
* Enables users to see content flags before submission.
*/
/**
* Default configuration
*/
export const DEFAULT_FLAGGING_CONFIG = {
threshold: 50,
enabledCategories: [
'profanity',
'hate_speech',
'spam',
'contact_info',
'solicitation',
'threats',
'scam_patterns',
],
categoryWeights: {
profanity: 0.5,
hate_speech: 2.0,
spam: 0.8,
contact_info: 1.0,
solicitation: 0.7,
threats: 2.5,
adult_content: 0.3,
scam_patterns: 1.5,
},
enableSentiment: true,
context: 'general',
};
/**
* Severity score mappings
*/
export const SEVERITY_SCORES = {
low: 5,
medium: 15,
high: 30,
critical: 50,
};

View file

@ -1,85 +0,0 @@
/**
* useAutosaveWithFlagging - Debounced autosave with content flagging
*
* Combines content flagging with autosave, showing toast notifications
* only when save state changes (not on every keystroke).
*/
import { type UseContentFlaggingOptions } from './useContentFlagging.js';
import type { ContentFlagResult } from './types.js';
export type AutosaveStatus = 'idle' | 'pending' | 'saving' | 'saved' | 'error' | 'blocked';
export interface AutosaveToastConfig {
/** Show toast on successful save */
onSave?: boolean;
/** Show toast on save error */
onError?: boolean;
/** Show toast when content is blocked by flagging */
onBlocked?: boolean;
/** Minimum time between toasts (prevents spam) */
debounceMs?: number;
}
export interface UseAutosaveWithFlaggingOptions extends Omit<UseContentFlaggingOptions, 'onFlagViolation' | 'onPass'> {
/** The save function */
onSave: (value: string) => Promise<void>;
/** Debounce delay before autosave (ms) */
autosaveDelayMs?: number;
/** Toast notification callbacks */
toast?: {
success?: (message: string) => void;
error?: (message: string) => void;
warning?: (message: string) => void;
loading?: (message: string) => string;
update?: (id: string, message: string, type: 'success' | 'error') => void;
dismiss?: (id: string) => void;
};
/** Toast configuration */
toastConfig?: AutosaveToastConfig;
/** Whether autosave is enabled */
autosaveEnabled?: boolean;
}
export interface UseAutosaveWithFlaggingReturn {
/** Current flag result */
flagResult: ContentFlagResult | null;
/** Whether content passes flagging */
passes: boolean;
/** Current flag score */
score: number;
/** Current autosave status */
status: AutosaveStatus;
/** Whether content is being analyzed */
isAnalyzing: boolean;
/** Whether save is in progress */
isSaving: boolean;
/** Last error message */
error: string | null;
/** Manually trigger save */
save: () => Promise<void>;
/** Reset status to idle */
reset: () => void;
}
/**
* Hook combining content flagging with debounced autosave and toast notifications
*
* @example
* ```tsx
* const { showToast, updateToast } = useToast()
*
* const { passes, status, score } = useAutosaveWithFlagging(bio, {
* threshold: 40,
* context: 'bio',
* autosaveDelayMs: 2000,
* onSave: async (value) => {
* await api.updateProfile({ bio: value })
* },
* toast: {
* success: (msg) => showToast(msg, 'success'),
* error: (msg) => showToast(msg, 'error'),
* warning: (msg) => showToast(msg, 'warning'),
* loading: (msg) => showToast(msg, 'loading'),
* update: (id, msg, type) => updateToast(id, msg, type),
* },
* })
* ```
*/
export declare function useAutosaveWithFlagging(value: string, options: UseAutosaveWithFlaggingOptions): UseAutosaveWithFlaggingReturn;
export default useAutosaveWithFlagging;
//# sourceMappingURL=useAutosaveWithFlagging.d.ts.map

View file

@ -1 +0,0 @@
{"version":3,"file":"useAutosaveWithFlagging.d.ts","sourceRoot":"","sources":["../src/useAutosaveWithFlagging.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,OAAO,EAAsB,KAAK,yBAAyB,EAAE,MAAM,yBAAyB,CAAA;AAE5F,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAA;AAEnD,MAAM,MAAM,cAAc,GAAG,MAAM,GAAG,SAAS,GAAG,QAAQ,GAAG,OAAO,GAAG,OAAO,GAAG,SAAS,CAAA;AAE1F,MAAM,WAAW,mBAAmB;IAClC,oCAAoC;IACpC,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,+BAA+B;IAC/B,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,qDAAqD;IACrD,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,kDAAkD;IAClD,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB;AAED,MAAM,WAAW,8BAA+B,SAAQ,IAAI,CAAC,yBAAyB,EAAE,iBAAiB,GAAG,QAAQ,CAAC;IACnH,wBAAwB;IACxB,MAAM,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;IACxC,0CAA0C;IAC1C,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,mCAAmC;IACnC,KAAK,CAAC,EAAE;QACN,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;QACnC,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;QACjC,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;QACnC,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,CAAA;QACrC,MAAM,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,GAAG,OAAO,KAAK,IAAI,CAAA;QACzE,OAAO,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAA;KAC/B,CAAA;IACD,0BAA0B;IAC1B,WAAW,CAAC,EAAE,mBAAmB,CAAA;IACjC,kCAAkC;IAClC,eAAe,CAAC,EAAE,OAAO,CAAA;CAC1B;AAED,MAAM,WAAW,6BAA6B;IAC5C,0BAA0B;IAC1B,UAAU,EAAE,iBAAiB,GAAG,IAAI,CAAA;IACpC,sCAAsC;IACtC,MAAM,EAAE,OAAO,CAAA;IACf,yBAAyB;IACzB,KAAK,EAAE,MAAM,CAAA;IACb,8BAA8B;IAC9B,MAAM,EAAE,cAAc,CAAA;IACtB,wCAAwC;IACxC,WAAW,EAAE,OAAO,CAAA;IACpB,kCAAkC;IAClC,QAAQ,EAAE,OAAO,CAAA;IACjB,yBAAyB;IACzB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,4BAA4B;IAC5B,IAAI,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;IACzB,2BAA2B;IAC3B,KAAK,EAAE,MAAM,IAAI,CAAA;CAClB;AASD;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,uBAAuB,CACrC,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,8BAA8B,GACtC,6BAA6B,CAyL/B;AAED,eAAe,uBAAuB,CAAA"}

View file

@ -1,191 +0,0 @@
/**
* useAutosaveWithFlagging - Debounced autosave with content flagging
*
* Combines content flagging with autosave, showing toast notifications
* only when save state changes (not on every keystroke).
*/
import { useCallback, useEffect, useRef, useState } from 'react';
import { useContentFlagging } from './useContentFlagging.js';
const DEFAULT_TOAST_CONFIG = {
onSave: true,
onError: true,
onBlocked: true,
debounceMs: 2000,
};
/**
* Hook combining content flagging with debounced autosave and toast notifications
*
* @example
* ```tsx
* const { showToast, updateToast } = useToast()
*
* const { passes, status, score } = useAutosaveWithFlagging(bio, {
* threshold: 40,
* context: 'bio',
* autosaveDelayMs: 2000,
* onSave: async (value) => {
* await api.updateProfile({ bio: value })
* },
* toast: {
* success: (msg) => showToast(msg, 'success'),
* error: (msg) => showToast(msg, 'error'),
* warning: (msg) => showToast(msg, 'warning'),
* loading: (msg) => showToast(msg, 'loading'),
* update: (id, msg, type) => updateToast(id, msg, type),
* },
* })
* ```
*/
export function useAutosaveWithFlagging(value, options) {
const { onSave, autosaveDelayMs = 2000, toast, toastConfig = DEFAULT_TOAST_CONFIG, autosaveEnabled = true,
// Flagging options
threshold, debounceMs, enabled, context, enabledCategories, categoryWeights, enableSentiment, whitelist, customWordLists, } = options;
const [status, setStatus] = useState('idle');
const [error, setError] = useState(null);
// Refs for debouncing
const autosaveTimerRef = useRef(null);
const lastSavedValueRef = useRef(value);
const lastToastTimeRef = useRef(0);
const loadingToastIdRef = useRef(null);
// Content flagging
const flagging = useContentFlagging(value, {
threshold,
debounceMs,
enabled,
context,
enabledCategories,
categoryWeights,
enableSentiment,
whitelist,
customWordLists,
});
// Debounced toast helper
const showDebouncedToast = useCallback((type, message) => {
const now = Date.now();
const minInterval = toastConfig.debounceMs ?? 2000;
if (now - lastToastTimeRef.current < minInterval) {
return; // Skip toast, too soon
}
lastToastTimeRef.current = now;
if (type === 'success' && toast?.success) {
toast.success(message);
}
else if (type === 'error' && toast?.error) {
toast.error(message);
}
else if (type === 'warning' && toast?.warning) {
toast.warning(message);
}
}, [toast, toastConfig.debounceMs]);
// Save function
const save = useCallback(async () => {
// Check if content passes flagging
if (!flagging.passes) {
setStatus('blocked');
if (toastConfig.onBlocked) {
showDebouncedToast('warning', 'Content flagged — cannot save');
}
return;
}
// Check if value changed
if (value === lastSavedValueRef.current) {
return; // No changes to save
}
setStatus('saving');
setError(null);
// Show loading toast if available
if (toast?.loading) {
loadingToastIdRef.current = toast.loading('Saving...');
}
try {
await onSave(value);
lastSavedValueRef.current = value;
setStatus('saved');
// Update or show success toast
if (loadingToastIdRef.current && toast?.update) {
toast.update(loadingToastIdRef.current, 'Saved', 'success');
loadingToastIdRef.current = null;
}
else if (toastConfig.onSave) {
showDebouncedToast('success', 'Saved');
}
}
catch (err) {
const errorMessage = err instanceof Error ? err.message : 'Failed to save';
setError(errorMessage);
setStatus('error');
// Update or show error toast
if (loadingToastIdRef.current && toast?.update) {
toast.update(loadingToastIdRef.current, errorMessage, 'error');
loadingToastIdRef.current = null;
}
else if (toastConfig.onError) {
showDebouncedToast('error', errorMessage);
}
}
}, [value, flagging.passes, onSave, toast, toastConfig, showDebouncedToast]);
// Reset function
const reset = useCallback(() => {
setStatus('idle');
setError(null);
if (loadingToastIdRef.current && toast?.dismiss) {
toast.dismiss(loadingToastIdRef.current);
loadingToastIdRef.current = null;
}
}, [toast]);
// Autosave effect
useEffect(() => {
if (!autosaveEnabled) {
return;
}
// Clear pending save
if (autosaveTimerRef.current) {
clearTimeout(autosaveTimerRef.current);
}
// Don't autosave empty content or if flagging is still analyzing
if (!value.trim() || flagging.isAnalyzing) {
return;
}
// Don't autosave if content doesn't pass
if (!flagging.passes) {
setStatus('blocked');
return;
}
// Check if value changed
if (value === lastSavedValueRef.current) {
return;
}
// Set pending status
setStatus('pending');
// Schedule autosave
autosaveTimerRef.current = setTimeout(() => {
save();
}, autosaveDelayMs);
return () => {
if (autosaveTimerRef.current) {
clearTimeout(autosaveTimerRef.current);
}
};
}, [value, autosaveEnabled, autosaveDelayMs, flagging.passes, flagging.isAnalyzing, save]);
// Update status when flagging state changes
useEffect(() => {
if (!flagging.passes && value !== lastSavedValueRef.current) {
setStatus('blocked');
}
else if (flagging.passes && status === 'blocked') {
setStatus('pending');
}
}, [flagging.passes, value, status]);
return {
flagResult: flagging.result,
passes: flagging.passes,
score: flagging.score,
status,
isAnalyzing: flagging.isAnalyzing,
isSaving: status === 'saving',
error,
save,
reset,
};
}
export default useAutosaveWithFlagging;

View file

@ -1,69 +0,0 @@
/**
* useContentFlagging React Hook
*
* Real-time content flagging hook for browser-side scoring.
* Debounced analysis with immediate feedback.
*/
import type { ContentFlagResult, ContentFlaggingConfig } from './types.js';
export interface UseContentFlaggingOptions extends Partial<ContentFlaggingConfig> {
/** Debounce delay in ms (default: 150) */
debounceMs?: number;
/** Enable analysis (can disable temporarily) */
enabled?: boolean;
/** Callback when content fails threshold */
onFlagViolation?: (result: ContentFlagResult) => void;
/** Callback when content passes */
onPass?: (result: ContentFlagResult) => void;
}
export interface UseContentFlaggingReturn {
/** Current flag result */
result: ContentFlagResult | null;
/** Whether content currently passes threshold */
passes: boolean;
/** Current score (0-100) */
score: number;
/** Whether analysis is in progress */
isAnalyzing: boolean;
/** Manually trigger analysis */
analyze: (text: string) => ContentFlagResult;
/** Reset state */
reset: () => void;
/** Update threshold */
setThreshold: (threshold: number) => void;
/** Current threshold */
threshold: number;
}
/**
* Hook for real-time content flagging
*
* @example
* ```tsx
* const { score, passes, result } = useContentFlagging(bioText, {
* threshold: 40,
* context: 'bio',
* onFlagViolation: (result) => {
* toast.warning(`Content flagged: ${result.flags[0]?.reason}`)
* }
* })
*
* return (
* <div>
* <textarea value={bioText} onChange={e => setBioText(e.target.value)} />
* <FlagScoreIndicator score={score} passes={passes} />
* {!passes && <span>Please modify content to reduce flag score</span>}
* <button disabled={!passes}>Save</button>
* </div>
* )
* ```
*/
export declare function useContentFlagging(text: string, options?: UseContentFlaggingOptions): UseContentFlaggingReturn;
/**
* Simpler hook that just returns pass/fail and score
* For use cases that don't need full flag details
*/
export declare function useContentScore(text: string, threshold?: number): {
score: number;
passes: boolean;
isAnalyzing: boolean;
};
//# sourceMappingURL=useContentFlagging.d.ts.map

View file

@ -1 +0,0 @@
{"version":3,"file":"useContentFlagging.d.ts","sourceRoot":"","sources":["../src/useContentFlagging.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAMH,OAAO,KAAK,EAAE,iBAAiB,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAA;AAE1E,MAAM,WAAW,yBAA0B,SAAQ,OAAO,CAAC,qBAAqB,CAAC;IAC/E,0CAA0C;IAC1C,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,gDAAgD;IAChD,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,4CAA4C;IAC5C,eAAe,CAAC,EAAE,CAAC,MAAM,EAAE,iBAAiB,KAAK,IAAI,CAAA;IACrD,mCAAmC;IACnC,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,iBAAiB,KAAK,IAAI,CAAA;CAC7C;AAED,MAAM,WAAW,wBAAwB;IACvC,0BAA0B;IAC1B,MAAM,EAAE,iBAAiB,GAAG,IAAI,CAAA;IAChC,iDAAiD;IACjD,MAAM,EAAE,OAAO,CAAA;IACf,4BAA4B;IAC5B,KAAK,EAAE,MAAM,CAAA;IACb,sCAAsC;IACtC,WAAW,EAAE,OAAO,CAAA;IACpB,gCAAgC;IAChC,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,iBAAiB,CAAA;IAC5C,kBAAkB;IAClB,KAAK,EAAE,MAAM,IAAI,CAAA;IACjB,uBAAuB;IACvB,YAAY,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAA;IACzC,wBAAwB;IACxB,SAAS,EAAE,MAAM,CAAA;CAClB;AAoBD;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,MAAM,EACZ,OAAO,GAAE,yBAA8B,GACtC,wBAAwB,CAkH1B;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAC7B,IAAI,EAAE,MAAM,EACZ,SAAS,SAAK,GACb;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,OAAO,CAAC;IAAC,WAAW,EAAE,OAAO,CAAA;CAAE,CAG1D"}

View file

@ -1,147 +0,0 @@
/**
* useContentFlagging React Hook
*
* Real-time content flagging hook for browser-side scoring.
* Debounced analysis with immediate feedback.
*/
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { ContentFlaggingService } from './ContentFlaggingService.js';
const EMPTY_RESULT = {
score: 0,
passes: true,
threshold: 50,
flags: [],
categoryScores: {
profanity: 0,
hate_speech: 0,
spam: 0,
contact_info: 0,
solicitation: 0,
threats: 0,
adult_content: 0,
scam_patterns: 0,
},
processingTimeMs: 0,
};
/**
* Hook for real-time content flagging
*
* @example
* ```tsx
* const { score, passes, result } = useContentFlagging(bioText, {
* threshold: 40,
* context: 'bio',
* onFlagViolation: (result) => {
* toast.warning(`Content flagged: ${result.flags[0]?.reason}`)
* }
* })
*
* return (
* <div>
* <textarea value={bioText} onChange={e => setBioText(e.target.value)} />
* <FlagScoreIndicator score={score} passes={passes} />
* {!passes && <span>Please modify content to reduce flag score</span>}
* <button disabled={!passes}>Save</button>
* </div>
* )
* ```
*/
export function useContentFlagging(text, options = {}) {
const { debounceMs = 150, enabled = true, onFlagViolation, onPass, ...configOptions } = options;
const [result, setResult] = useState(null);
const [isAnalyzing, setIsAnalyzing] = useState(false);
const [threshold, setThresholdState] = useState(options.threshold ?? 50);
const debounceRef = useRef(null);
const prevPassesRef = useRef(null);
// Create service instance (memoized)
const service = useMemo(() => new ContentFlaggingService({
...configOptions,
threshold,
}), [
threshold,
options.context,
options.enabledCategories?.join(','),
options.enableSentiment,
]);
// Manual analyze function
const analyze = useCallback((textToAnalyze) => {
const analysisResult = service.analyze(textToAnalyze);
setResult(analysisResult);
return analysisResult;
}, [service]);
// Reset function
const reset = useCallback(() => {
setResult(null);
prevPassesRef.current = null;
}, []);
// Set threshold
const setThreshold = useCallback((newThreshold) => {
setThresholdState(Math.max(0, Math.min(100, newThreshold)));
}, []);
// Debounced analysis effect
useEffect(() => {
if (!enabled) {
return;
}
// Clear pending debounce
if (debounceRef.current) {
clearTimeout(debounceRef.current);
}
// Empty text - immediate result
if (!text || text.trim().length === 0) {
setResult({ ...EMPTY_RESULT, threshold });
setIsAnalyzing(false);
return;
}
setIsAnalyzing(true);
// Debounce the analysis
debounceRef.current = setTimeout(() => {
const analysisResult = service.analyze(text);
setResult(analysisResult);
setIsAnalyzing(false);
// Fire callbacks on state change
const currentPasses = analysisResult.passes;
const previousPasses = prevPassesRef.current;
if (previousPasses !== null && previousPasses !== currentPasses) {
if (currentPasses && onPass) {
onPass(analysisResult);
}
else if (!currentPasses && onFlagViolation) {
onFlagViolation(analysisResult);
}
}
else if (previousPasses === null && !currentPasses && onFlagViolation) {
// Initial analysis failed
onFlagViolation(analysisResult);
}
prevPassesRef.current = currentPasses;
}, debounceMs);
return () => {
if (debounceRef.current) {
clearTimeout(debounceRef.current);
}
};
}, [text, enabled, debounceMs, service, threshold, onFlagViolation, onPass]);
// Update service threshold when it changes
useEffect(() => {
service.setThreshold(threshold);
}, [service, threshold]);
return {
result,
passes: result?.passes ?? true,
score: result?.score ?? 0,
isAnalyzing,
analyze,
reset,
setThreshold,
threshold,
};
}
/**
* Simpler hook that just returns pass/fail and score
* For use cases that don't need full flag details
*/
export function useContentScore(text, threshold = 50) {
const { score, passes, isAnalyzing } = useContentFlagging(text, { threshold });
return { score, passes, isAnalyzing };
}