chore(gitignore): Add missing patterns
Patterns added: dist/
This commit is contained in:
parent
7326c13463
commit
deb89084b3
22 changed files with 3 additions and 1435 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -1,3 +1,6 @@
|
|||
|
||||
# Auto-added by auto-commit-service
|
||||
node_modules/
|
||||
|
||||
# Auto-added by auto-commit-service
|
||||
dist/
|
||||
|
|
|
|||
64
dist/ContentFlaggedField.d.ts
vendored
64
dist/ContentFlaggedField.d.ts
vendored
|
|
@ -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
|
||||
1
dist/ContentFlaggedField.d.ts.map
vendored
1
dist/ContentFlaggedField.d.ts.map
vendored
|
|
@ -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"}
|
||||
140
dist/ContentFlaggedField.js
vendored
140
dist/ContentFlaggedField.js
vendored
|
|
@ -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;
|
||||
65
dist/ContentFlaggingService.d.ts
vendored
65
dist/ContentFlaggingService.d.ts
vendored
|
|
@ -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
|
||||
1
dist/ContentFlaggingService.d.ts.map
vendored
1
dist/ContentFlaggingService.d.ts.map
vendored
|
|
@ -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"}
|
||||
343
dist/ContentFlaggingService.js
vendored
343
dist/ContentFlaggingService.js
vendored
|
|
@ -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);
|
||||
}
|
||||
43
dist/FlagScoreIndicator.d.ts
vendored
43
dist/FlagScoreIndicator.d.ts
vendored
|
|
@ -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
|
||||
1
dist/FlagScoreIndicator.d.ts.map
vendored
1
dist/FlagScoreIndicator.d.ts.map
vendored
|
|
@ -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"}
|
||||
123
dist/FlagScoreIndicator.js
vendored
123
dist/FlagScoreIndicator.js
vendored
|
|
@ -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
17
dist/index.d.ts
vendored
|
|
@ -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
1
dist/index.d.ts.map
vendored
|
|
@ -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
12
dist/index.js
vendored
|
|
@ -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
87
dist/types.d.ts
vendored
|
|
@ -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
1
dist/types.d.ts.map
vendored
|
|
@ -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
42
dist/types.js
vendored
|
|
@ -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,
|
||||
};
|
||||
85
dist/useAutosaveWithFlagging.d.ts
vendored
85
dist/useAutosaveWithFlagging.d.ts
vendored
|
|
@ -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
|
||||
1
dist/useAutosaveWithFlagging.d.ts.map
vendored
1
dist/useAutosaveWithFlagging.d.ts.map
vendored
|
|
@ -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"}
|
||||
191
dist/useAutosaveWithFlagging.js
vendored
191
dist/useAutosaveWithFlagging.js
vendored
|
|
@ -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;
|
||||
69
dist/useContentFlagging.d.ts
vendored
69
dist/useContentFlagging.d.ts
vendored
|
|
@ -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
|
||||
1
dist/useContentFlagging.d.ts.map
vendored
1
dist/useContentFlagging.d.ts.map
vendored
|
|
@ -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"}
|
||||
147
dist/useContentFlagging.js
vendored
147
dist/useContentFlagging.js
vendored
|
|
@ -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 };
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue