mirror of
https://github.com/civitai/civitai.git
synced 2026-09-20 22:08:18 +08:00
feat(shared): add @civitai/shared package (Flags + browsing-levels)
New client-safe, dependency-free package for cross-app pure constants + utilities, wired into the main app (transpilePackages + workspace dep). First shared-package home for these primitives; app-level copies can adopt it to dedup later. - flags.ts: the Flags bitwise utility (port of the main app's ~/shared/utils/flags). - browsing-levels.ts: NsfwLevel bitwise levels + the generic browsing-level toolkit ported from ~/shared/constants/browsingLevel.constants — level groupings + flags, parseBitwiseBrowsingLevel/flagifyBrowsingLevel, labels + descriptions, severity/label helpers, and the browsing-level predicates. The App-Blocks/orchestrator/deprecated/UI/ reasons pieces stay app-specific. - allBrowsingLevelsFlag matches the main app (EXCLUDES Blocked); a distinct allBrowsingLevelsWithBlockedFlag covers moderator "show Blocked" contexts. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -141,6 +141,7 @@ export default defineNextConfig(
|
||||
'superjson',
|
||||
'@civitai/db-schema',
|
||||
'@civitai/db',
|
||||
'@civitai/shared',
|
||||
'@civitai/buzz',
|
||||
'@civitai/redis',
|
||||
'@civitai/clickhouse',
|
||||
|
||||
@@ -111,6 +111,7 @@
|
||||
"@civitai/client": "0.2.0-beta.81",
|
||||
"@civitai/cybertipline-tools": "^0.1.0",
|
||||
"@civitai/next-axiom": "^0.17.0",
|
||||
"@civitai/shared": "workspace:*",
|
||||
"@clavata/sdk": "^0.2.3",
|
||||
"@clickhouse/client": "^0.2.2",
|
||||
"@coinbase/cdp-sdk": "^1.13.0",
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"name": "@civitai/shared",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"main": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
import { Flags } from './flags';
|
||||
|
||||
// Numeric (bitwise) NSFW levels + the generic browsing-level toolkit — a faithful port of the reusable
|
||||
// (non-product-specific) parts of the main app's `~/shared/constants/browsingLevel.constants` and its
|
||||
// `~/server/common/enums` NsfwLevel. These are stable app constants, not Prisma enums, so @civitai/db-schema
|
||||
// doesn't carry them. Client-safe (no server/env/framework deps) so any app can import them — server code,
|
||||
// SvelteKit components, and the main Next app alike.
|
||||
//
|
||||
// DELIBERATELY EXCLUDED here (main-app / product / UI specific — keep those in the main app): the App-Blocks
|
||||
// off-site content-rating ladder + domain ceilings, the orchestrator level map, the deprecated NsfwLevel
|
||||
// enum + maps, Mantine color maps (nsfwLevelColors / votableTagColors), toggleable browsing categories,
|
||||
// browsingModeDefaults, and the per-level moderation "reasons" copy.
|
||||
export const NsfwLevel = {
|
||||
PG: 1,
|
||||
PG13: 2,
|
||||
R: 4,
|
||||
X: 8,
|
||||
XXX: 16,
|
||||
Blocked: 32,
|
||||
} as const;
|
||||
|
||||
export function parseBitwiseBrowsingLevel(level: number): number[] {
|
||||
return Flags.instanceToArray(level);
|
||||
}
|
||||
|
||||
export function flagifyBrowsingLevel(levels: number[]) {
|
||||
return Flags.arrayToInstance(levels);
|
||||
}
|
||||
|
||||
export type BrowsingLevels = typeof browsingLevels;
|
||||
export type BrowsingLevel = BrowsingLevels[number];
|
||||
export const browsingLevels = [
|
||||
NsfwLevel.PG,
|
||||
NsfwLevel.PG13,
|
||||
NsfwLevel.R,
|
||||
NsfwLevel.X,
|
||||
NsfwLevel.XXX,
|
||||
] as const;
|
||||
|
||||
export const browsingLevelLabels = {
|
||||
0: '?',
|
||||
[NsfwLevel.PG]: 'PG',
|
||||
[NsfwLevel.PG13]: 'PG-13',
|
||||
[NsfwLevel.R]: 'R',
|
||||
[NsfwLevel.X]: 'X',
|
||||
[NsfwLevel.XXX]: 'XXX',
|
||||
[NsfwLevel.Blocked]: 'Blocked',
|
||||
} as const;
|
||||
|
||||
export const browsingLevelDescriptions = {
|
||||
[NsfwLevel.PG]: 'Safe for work. No naughty stuff',
|
||||
[NsfwLevel.PG13]:
|
||||
'Revealing clothing, small bulges, subtle nipple outline, posing/sexualized bare chested men, light gore, violence',
|
||||
[NsfwLevel.R]:
|
||||
'Adult themes and situations, partial nudity, bikinis, big bulges, sexual situations, graphic violence',
|
||||
[NsfwLevel.X]: 'Graphic nudity, genitalia, adult objects, or settings',
|
||||
[NsfwLevel.XXX]:
|
||||
'Sexual Acts, masturbation, ejaculation, cum, vore, anal gape, extremely disturbing content',
|
||||
[NsfwLevel.Blocked]: 'Violates our terms of service',
|
||||
} as const;
|
||||
|
||||
// Level groupings + their OR'd flags.
|
||||
export const publicBrowsingLevelsArray: BrowsingLevel[] = [NsfwLevel.PG];
|
||||
export const publicBrowsingLevelsFlag = flagifyBrowsingLevel(publicBrowsingLevelsArray);
|
||||
|
||||
export const sfwBrowsingLevelsArray: BrowsingLevel[] = [NsfwLevel.PG, NsfwLevel.PG13];
|
||||
export const sfwBrowsingLevelsFlag = flagifyBrowsingLevel(sfwBrowsingLevelsArray);
|
||||
|
||||
export const nsfwBrowsingLevelsArray: number[] = [
|
||||
NsfwLevel.R,
|
||||
NsfwLevel.X,
|
||||
NsfwLevel.XXX,
|
||||
NsfwLevel.Blocked,
|
||||
];
|
||||
export const nsfwBrowsingLevelsFlag = flagifyBrowsingLevel(nsfwBrowsingLevelsArray);
|
||||
|
||||
// All rateable levels OR'd together — matches the main app's `allBrowsingLevelsFlag` (EXCLUDES Blocked;
|
||||
// Blocked is a TOS action, not a rating).
|
||||
export const allBrowsingLevelsFlag = flagifyBrowsingLevel([...browsingLevels]);
|
||||
|
||||
// All levels INCLUDING Blocked — for moderator contexts (e.g. a review-queue browsing filter that shows
|
||||
// Blocked too). Distinct from `allBrowsingLevelsFlag` on purpose.
|
||||
export const allBrowsingLevelsWithBlockedFlag = allBrowsingLevelsFlag | NsfwLevel.Blocked;
|
||||
|
||||
// Highest-severity bit first — `getHighestBrowsingLevelBit` returns the most severe single-bit value in a
|
||||
// composite, which is what `getBrowsingLevelLabel` uses to label aggregate levels (e.g. a composite of
|
||||
// PG | R = 5, which has no direct entry in `browsingLevelLabels`).
|
||||
const browsingLevelBitsBySeverity: number[] = [
|
||||
NsfwLevel.Blocked,
|
||||
NsfwLevel.XXX,
|
||||
NsfwLevel.X,
|
||||
NsfwLevel.R,
|
||||
NsfwLevel.PG13,
|
||||
NsfwLevel.PG,
|
||||
];
|
||||
|
||||
export function getHighestBrowsingLevelBit(value: number): number {
|
||||
for (const bit of browsingLevelBitsBySeverity) {
|
||||
if ((value & bit) !== 0) return bit;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
export function getBrowsingLevelLabel(value: number | null | undefined): string {
|
||||
if (!value) return '?';
|
||||
const direct = browsingLevelLabels[value as keyof typeof browsingLevelLabels];
|
||||
if (direct) return direct;
|
||||
const highest = getHighestBrowsingLevelBit(value);
|
||||
return highest ? browsingLevelLabels[highest as keyof typeof browsingLevelLabels] : '?';
|
||||
}
|
||||
|
||||
// --- predicates / helpers ---
|
||||
|
||||
// Strip the Blocked bit (a TOS action, not a selectable rating); removeFlag is a no-op when it isn't set.
|
||||
export function onlySelectableLevels(level: number) {
|
||||
return Flags.removeFlag(level, NsfwLevel.Blocked);
|
||||
}
|
||||
|
||||
// True when `level` contains only public bits (its bits are a subset of publicBrowsingLevelsFlag).
|
||||
export function getIsPublicBrowsingLevel(level: number) {
|
||||
return Flags.diff(level, publicBrowsingLevelsFlag) === 0;
|
||||
}
|
||||
|
||||
/** does not include any nsfw level flags */
|
||||
export function getIsSafeBrowsingLevel(level: number) {
|
||||
return level !== 0 && !Flags.intersects(level, nsfwBrowsingLevelsFlag);
|
||||
}
|
||||
|
||||
/** includes a level suitable for public browsing */
|
||||
export function hasPublicBrowsingLevel(level: number) {
|
||||
return Flags.hasFlag(level, publicBrowsingLevelsFlag);
|
||||
}
|
||||
|
||||
export function hasSafeBrowsingLevel(level: number) {
|
||||
return Flags.intersects(level, sfwBrowsingLevelsFlag);
|
||||
}
|
||||
|
||||
const explicitBrowsingLevelFlags = flagifyBrowsingLevel([
|
||||
NsfwLevel.X,
|
||||
NsfwLevel.XXX,
|
||||
NsfwLevel.Blocked,
|
||||
]);
|
||||
export function getHasExplicitBrowsingLevel(level: number) {
|
||||
return Flags.intersects(level, explicitBrowsingLevelFlags);
|
||||
}
|
||||
|
||||
export const browsingLevelOr = (array: (number | undefined)[]) =>
|
||||
array.find((x) => !!x) ?? publicBrowsingLevelsFlag;
|
||||
|
||||
// --- moderator-facing level sets (used by the moderator app; not in the main app's constants) ---
|
||||
|
||||
// Single-bit levels a moderator can pin content to (excludes Blocked; that's a TOS action, not a rating).
|
||||
// Callers should re-validate server-side.
|
||||
export const validNsfwLevels = new Set<number>(browsingLevels);
|
||||
|
||||
// Ingestion-error review lets a moderator set any browsing level OR Blocked (a mis-ingested image may be
|
||||
// TOS-violating), so this set is broader than validNsfwLevels.
|
||||
export const ingestionErrorLevels = [...browsingLevels, NsfwLevel.Blocked] as const;
|
||||
export const ingestionErrorLevelSet = new Set<number>(ingestionErrorLevels);
|
||||
@@ -0,0 +1,73 @@
|
||||
// Pure bitwise-flag helpers, no deps — client-safe. Mirror of the main app's `~/shared/utils/flags`, moved
|
||||
// here so cross-app modules (e.g. browsing-levels) can share one implementation.
|
||||
export abstract class Flags {
|
||||
private static possibleValues: number[] = (() =>
|
||||
[...new Array(32)].map((_, i) => Math.pow(2, i)))();
|
||||
|
||||
/** true if every bit set in `flag` is also set in `instance`. */
|
||||
static hasFlag(instance: number, flag: number) {
|
||||
return (instance | flag) === instance;
|
||||
}
|
||||
|
||||
/** the bits shared between two instances. */
|
||||
static intersection(instance1: number, instance2: number) {
|
||||
return instance1 & instance2;
|
||||
}
|
||||
|
||||
static intersects(instance1: number, instance2: number) {
|
||||
return (instance1 & instance2) !== 0;
|
||||
}
|
||||
|
||||
static addFlag(instance: number, flag: number) {
|
||||
return instance | flag;
|
||||
}
|
||||
|
||||
static removeFlag(instance: number, flag: number) {
|
||||
return instance & ~flag;
|
||||
}
|
||||
|
||||
static maxValue(flag: number) {
|
||||
return Math.max(...this.instanceToArray(flag));
|
||||
}
|
||||
|
||||
static toggleFlag(instance: number, flag: number) {
|
||||
return this.hasFlag(instance, flag)
|
||||
? this.removeFlag(instance, flag)
|
||||
: this.addFlag(instance, flag);
|
||||
}
|
||||
|
||||
/** enum object → array of its numeric values, e.g. `{ user: 1, admin: 4 }` → `[1, 4]`. */
|
||||
static enumToBitArray(enumValue: object) {
|
||||
return Object.keys(enumValue).map(Number).filter(Boolean);
|
||||
}
|
||||
|
||||
/** instance → array of the set bits, e.g. `11` → `[1, 2, 8]`. */
|
||||
static instanceToArray(instance: number) {
|
||||
return this.possibleValues.filter((x) => this.hasFlag(instance, x));
|
||||
}
|
||||
|
||||
/** array of bit values → instance, e.g. `[1, 2, 4]` → `7`. */
|
||||
static arrayToInstance(flagsArray: number[]) {
|
||||
return flagsArray.reduce((agg, cur) => {
|
||||
const toAdd = this.possibleValues.includes(cur) ? cur : 0;
|
||||
return agg + toAdd;
|
||||
}, 0);
|
||||
}
|
||||
|
||||
/** the bitwise difference between two values. */
|
||||
static diff(a: number, b: number) {
|
||||
return a & ~b;
|
||||
}
|
||||
|
||||
/** the number of bit positions between two single-bit flag values. */
|
||||
static distance(a: number, b: number): number {
|
||||
const pos1 = Math.log2(a);
|
||||
const pos2 = Math.log2(b);
|
||||
|
||||
return Math.abs(pos1 - pos2);
|
||||
}
|
||||
|
||||
static increaseByBits(instance: number, bits = 1) {
|
||||
return instance << bits;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
// @civitai/shared — cross-app pure constants + utilities (no DB / env / framework deps). Client-safe, so
|
||||
// server code, SvelteKit components, and the main Next app can all import from here. Add new broadly
|
||||
// shared primitives as their own modules and re-export them below.
|
||||
export * from './flags';
|
||||
export * from './browsing-levels';
|
||||
Generated
+5
@@ -46,6 +46,9 @@ importers:
|
||||
'@civitai/next-axiom':
|
||||
specifier: ^0.17.0
|
||||
version: 0.17.0(next@16.2.7(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.90.0))
|
||||
'@civitai/shared':
|
||||
specifier: workspace:*
|
||||
version: link:packages/civitai-shared
|
||||
'@clavata/sdk':
|
||||
specifier: ^0.2.3
|
||||
version: 0.2.3
|
||||
@@ -1263,6 +1266,8 @@ importers:
|
||||
specifier: ^4.0.18
|
||||
version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.9)(@vitest/browser-playwright@4.0.18)(happy-dom@20.9.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(jiti@2.7.0)(jsdom@27.4.0(@noble/hashes@1.8.0)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(lightningcss@1.32.0)(msw@2.12.10(@types/node@20.19.9)(typescript@5.9.3))(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.48.0)(tsx@4.20.3)(yaml@2.8.1)
|
||||
|
||||
packages/civitai-shared: {}
|
||||
|
||||
packages/civitai-storage:
|
||||
dependencies:
|
||||
zod:
|
||||
|
||||
Reference in New Issue
Block a user