mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-28 18:22:40 +00:00
Cloud/tracking v2 (#6400)
## Summary <!-- One sentence describing what changed and why. --> ## Changes - **What**: <!-- Core functionality added/modified --> - **Breaking**: <!-- Any breaking changes (if none, remove this line) --> - **Dependencies**: <!-- New dependencies (if none, remove this line) --> ## Review Focus <!-- Critical design decisions or edge cases that need attention --> <!-- If this PR fixes an issue, uncomment and update the line below --> <!-- Fixes #ISSUE_NUMBER --> ## Screenshots (if applicable) <!-- Add screenshots or video recording to help explain your changes --> ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-6400-Cloud-tracking-v2-29c6d73d365081a1ae32e9337f510a9e) by [Unito](https://www.unito.io) --------- Co-authored-by: Arjan Singh <arjan@comfy.org>
This commit is contained in:
@@ -3,18 +3,31 @@ import type { OverridedMixpanel } from 'mixpanel-browser'
|
||||
import { useCurrentUser } from '@/composables/auth/useCurrentUser'
|
||||
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
|
||||
import { useWorkflowTemplatesStore } from '@/platform/workflow/templates/repositories/workflowTemplatesStore'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useNodeDefStore } from '@/stores/nodeDefStore'
|
||||
import { NodeSourceType } from '@/types/nodeSource'
|
||||
import { reduceAllNodes } from '@/utils/graphTraversalUtil'
|
||||
import { normalizeSurveyResponses } from '../../utils/surveyNormalization'
|
||||
|
||||
import type {
|
||||
AuthMetadata,
|
||||
CreditTopupMetadata,
|
||||
ExecutionContext,
|
||||
ExecutionErrorMetadata,
|
||||
ExecutionSuccessMetadata,
|
||||
NodeSearchMetadata,
|
||||
NodeSearchResultMetadata,
|
||||
PageVisibilityMetadata,
|
||||
RunButtonProperties,
|
||||
SurveyResponses,
|
||||
TabCountMetadata,
|
||||
TelemetryEventName,
|
||||
TelemetryEventProperties,
|
||||
TelemetryProvider,
|
||||
TemplateMetadata
|
||||
TemplateFilterMetadata,
|
||||
TemplateLibraryMetadata,
|
||||
TemplateMetadata,
|
||||
WorkflowImportMetadata
|
||||
} from '../../types'
|
||||
import { TelemetryEvents } from '../../types'
|
||||
|
||||
@@ -123,6 +136,10 @@ export class MixpanelTelemetryProvider implements TelemetryProvider {
|
||||
this.trackEvent(TelemetryEvents.USER_AUTH_COMPLETED, metadata)
|
||||
}
|
||||
|
||||
trackUserLoggedIn(): void {
|
||||
this.trackEvent(TelemetryEvents.USER_LOGGED_IN)
|
||||
}
|
||||
|
||||
trackSubscription(event: 'modal_opened' | 'subscribe_clicked'): void {
|
||||
const eventName =
|
||||
event === 'modal_opened'
|
||||
@@ -132,6 +149,20 @@ export class MixpanelTelemetryProvider implements TelemetryProvider {
|
||||
this.trackEvent(eventName)
|
||||
}
|
||||
|
||||
trackMonthlySubscriptionSucceeded(): void {
|
||||
this.trackEvent(TelemetryEvents.MONTHLY_SUBSCRIPTION_SUCCEEDED)
|
||||
}
|
||||
|
||||
trackApiCreditTopupButtonPurchaseClicked(amount: number): void {
|
||||
const metadata: CreditTopupMetadata = {
|
||||
credit_amount: amount
|
||||
}
|
||||
this.trackEvent(
|
||||
TelemetryEvents.API_CREDIT_TOPUP_BUTTON_PURCHASE_CLICKED,
|
||||
metadata
|
||||
)
|
||||
}
|
||||
|
||||
trackRunButton(options?: { subscribe_to_run?: boolean }): void {
|
||||
const executionContext = this.getExecutionContext()
|
||||
|
||||
@@ -153,7 +184,21 @@ export class MixpanelTelemetryProvider implements TelemetryProvider {
|
||||
? TelemetryEvents.USER_SURVEY_OPENED
|
||||
: TelemetryEvents.USER_SURVEY_SUBMITTED
|
||||
|
||||
this.trackEvent(eventName, responses)
|
||||
// Apply normalization to survey responses
|
||||
const normalizedResponses = responses
|
||||
? normalizeSurveyResponses(responses)
|
||||
: undefined
|
||||
|
||||
this.trackEvent(eventName, normalizedResponses)
|
||||
|
||||
// If this is a survey submission, also set user properties with normalized data
|
||||
if (stage === 'submitted' && normalizedResponses && this.mixpanel) {
|
||||
try {
|
||||
this.mixpanel.people.set(normalizedResponses)
|
||||
} catch (error) {
|
||||
console.error('Failed to set survey user properties:', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
trackEmailVerification(stage: 'opened' | 'requested' | 'completed'): void {
|
||||
@@ -178,6 +223,34 @@ export class MixpanelTelemetryProvider implements TelemetryProvider {
|
||||
this.trackEvent(TelemetryEvents.TEMPLATE_WORKFLOW_OPENED, metadata)
|
||||
}
|
||||
|
||||
trackTemplateLibraryOpened(metadata: TemplateLibraryMetadata): void {
|
||||
this.trackEvent(TelemetryEvents.TEMPLATE_LIBRARY_OPENED, metadata)
|
||||
}
|
||||
|
||||
trackWorkflowImported(metadata: WorkflowImportMetadata): void {
|
||||
this.trackEvent(TelemetryEvents.WORKFLOW_IMPORTED, metadata)
|
||||
}
|
||||
|
||||
trackPageVisibilityChanged(metadata: PageVisibilityMetadata): void {
|
||||
this.trackEvent(TelemetryEvents.PAGE_VISIBILITY_CHANGED, metadata)
|
||||
}
|
||||
|
||||
trackTabCount(metadata: TabCountMetadata): void {
|
||||
this.trackEvent(TelemetryEvents.TAB_COUNT_TRACKING, metadata)
|
||||
}
|
||||
|
||||
trackNodeSearch(metadata: NodeSearchMetadata): void {
|
||||
this.trackEvent(TelemetryEvents.NODE_SEARCH, metadata)
|
||||
}
|
||||
|
||||
trackNodeSearchResultSelected(metadata: NodeSearchResultMetadata): void {
|
||||
this.trackEvent(TelemetryEvents.NODE_SEARCH_RESULT_SELECTED, metadata)
|
||||
}
|
||||
|
||||
trackTemplateFilterChanged(metadata: TemplateFilterMetadata): void {
|
||||
this.trackEvent(TelemetryEvents.TEMPLATE_FILTER_CHANGED, metadata)
|
||||
}
|
||||
|
||||
trackWorkflowExecution(): void {
|
||||
const context = this.getExecutionContext()
|
||||
this.trackEvent(TelemetryEvents.EXECUTION_START, context)
|
||||
@@ -194,8 +267,28 @@ export class MixpanelTelemetryProvider implements TelemetryProvider {
|
||||
getExecutionContext(): ExecutionContext {
|
||||
const workflowStore = useWorkflowStore()
|
||||
const templatesStore = useWorkflowTemplatesStore()
|
||||
const nodeDefStore = useNodeDefStore()
|
||||
const activeWorkflow = workflowStore.activeWorkflow
|
||||
|
||||
// Calculate node metrics in a single traversal
|
||||
const nodeMetrics = reduceAllNodes(
|
||||
app.graph,
|
||||
(acc, node) => {
|
||||
const nodeDef = nodeDefStore.nodeDefsByName[node.type]
|
||||
const isCustomNode =
|
||||
nodeDef?.nodeSource?.type === NodeSourceType.CustomNodes
|
||||
const isApiNode = nodeDef?.api_node === true
|
||||
const isSubgraph = node.isSubgraphNode?.() === true
|
||||
|
||||
return {
|
||||
custom_node_count: acc.custom_node_count + (isCustomNode ? 1 : 0),
|
||||
api_node_count: acc.api_node_count + (isApiNode ? 1 : 0),
|
||||
subgraph_count: acc.subgraph_count + (isSubgraph ? 1 : 0)
|
||||
}
|
||||
},
|
||||
{ custom_node_count: 0, api_node_count: 0, subgraph_count: 0 }
|
||||
)
|
||||
|
||||
if (activeWorkflow?.filename) {
|
||||
const isTemplate = templatesStore.knownTemplateNames.has(
|
||||
activeWorkflow.filename
|
||||
@@ -218,19 +311,22 @@ export class MixpanelTelemetryProvider implements TelemetryProvider {
|
||||
template_tags: englishMetadata?.tags ?? template?.tags,
|
||||
template_models: englishMetadata?.models ?? template?.models,
|
||||
template_use_case: englishMetadata?.useCase ?? template?.useCase,
|
||||
template_license: englishMetadata?.license ?? template?.license
|
||||
template_license: englishMetadata?.license ?? template?.license,
|
||||
...nodeMetrics
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
is_template: false,
|
||||
workflow_name: activeWorkflow.filename
|
||||
workflow_name: activeWorkflow.filename,
|
||||
...nodeMetrics
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
is_template: false,
|
||||
workflow_name: undefined
|
||||
workflow_name: undefined,
|
||||
...nodeMetrics
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,6 +57,10 @@ export interface ExecutionContext {
|
||||
template_models?: string[]
|
||||
template_use_case?: string
|
||||
template_license?: string
|
||||
// Node composition metrics
|
||||
custom_node_count: number
|
||||
api_node_count: number
|
||||
subgraph_count: number
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -89,15 +93,87 @@ export interface TemplateMetadata {
|
||||
template_license?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Credit topup metadata
|
||||
*/
|
||||
export interface CreditTopupMetadata {
|
||||
credit_amount: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Workflow import metadata
|
||||
*/
|
||||
export interface WorkflowImportMetadata {
|
||||
missing_node_count: number
|
||||
missing_node_types: string[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Template library metadata
|
||||
*/
|
||||
export interface TemplateLibraryMetadata {
|
||||
source: 'sidebar' | 'menu' | 'command'
|
||||
}
|
||||
|
||||
/**
|
||||
* Page visibility metadata
|
||||
*/
|
||||
export interface PageVisibilityMetadata {
|
||||
visibility_state: 'visible' | 'hidden'
|
||||
}
|
||||
|
||||
/**
|
||||
* Tab count metadata
|
||||
*/
|
||||
export interface TabCountMetadata {
|
||||
tab_count: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Node search metadata
|
||||
*/
|
||||
export interface NodeSearchMetadata {
|
||||
query: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Node search result selection metadata
|
||||
*/
|
||||
export interface NodeSearchResultMetadata {
|
||||
node_type: string
|
||||
last_query: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Template filter tracking metadata
|
||||
*/
|
||||
export interface TemplateFilterMetadata {
|
||||
search_query?: string
|
||||
selected_models: string[]
|
||||
selected_use_cases: string[]
|
||||
selected_licenses: string[]
|
||||
sort_by:
|
||||
| 'default'
|
||||
| 'alphabetical'
|
||||
| 'newest'
|
||||
| 'vram-low-to-high'
|
||||
| 'model-size-low-to-high'
|
||||
filtered_count: number
|
||||
total_count: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Core telemetry provider interface
|
||||
*/
|
||||
export interface TelemetryProvider {
|
||||
// Authentication flow events
|
||||
trackAuth(metadata: AuthMetadata): void
|
||||
trackUserLoggedIn(): void
|
||||
|
||||
// Subscription flow events
|
||||
trackSubscription(event: 'modal_opened' | 'subscribe_clicked'): void
|
||||
trackMonthlySubscriptionSucceeded(): void
|
||||
trackApiCreditTopupButtonPurchaseClicked(amount: number): void
|
||||
trackRunButton(options?: { subscribe_to_run?: boolean }): void
|
||||
|
||||
// Survey flow events
|
||||
@@ -108,6 +184,23 @@ export interface TelemetryProvider {
|
||||
|
||||
// Template workflow events
|
||||
trackTemplate(metadata: TemplateMetadata): void
|
||||
trackTemplateLibraryOpened(metadata: TemplateLibraryMetadata): void
|
||||
|
||||
// Workflow management events
|
||||
trackWorkflowImported(metadata: WorkflowImportMetadata): void
|
||||
|
||||
// Page visibility events
|
||||
trackPageVisibilityChanged(metadata: PageVisibilityMetadata): void
|
||||
|
||||
// Tab tracking events
|
||||
trackTabCount(metadata: TabCountMetadata): void
|
||||
|
||||
// Node search analytics events
|
||||
trackNodeSearch(metadata: NodeSearchMetadata): void
|
||||
trackNodeSearchResultSelected(metadata: NodeSearchResultMetadata): void
|
||||
|
||||
// Template filter tracking events
|
||||
trackTemplateFilterChanged(metadata: TemplateFilterMetadata): void
|
||||
|
||||
// Workflow execution events
|
||||
trackWorkflowExecution(): void
|
||||
@@ -125,11 +218,15 @@ export interface TelemetryProvider {
|
||||
export const TelemetryEvents = {
|
||||
// Authentication Flow
|
||||
USER_AUTH_COMPLETED: 'app:user_auth_completed',
|
||||
USER_LOGGED_IN: 'app:user_logged_in',
|
||||
|
||||
// Subscription Flow
|
||||
RUN_BUTTON_CLICKED: 'app:run_button_click',
|
||||
SUBSCRIPTION_REQUIRED_MODAL_OPENED: 'app:subscription_required_modal_opened',
|
||||
SUBSCRIBE_NOW_BUTTON_CLICKED: 'app:subscribe_now_button_clicked',
|
||||
MONTHLY_SUBSCRIPTION_SUCCEEDED: 'app:monthly_subscription_succeeded',
|
||||
API_CREDIT_TOPUP_BUTTON_PURCHASE_CLICKED:
|
||||
'app:api_credit_topup_button_purchase_clicked',
|
||||
|
||||
// Onboarding Survey
|
||||
USER_SURVEY_OPENED: 'app:user_survey_opened',
|
||||
@@ -142,6 +239,23 @@ export const TelemetryEvents = {
|
||||
|
||||
// Template Tracking
|
||||
TEMPLATE_WORKFLOW_OPENED: 'app:template_workflow_opened',
|
||||
TEMPLATE_LIBRARY_OPENED: 'app:template_library_opened',
|
||||
|
||||
// Workflow Management
|
||||
WORKFLOW_IMPORTED: 'app:workflow_imported',
|
||||
|
||||
// Page Visibility
|
||||
PAGE_VISIBILITY_CHANGED: 'app:page_visibility_changed',
|
||||
|
||||
// Tab Tracking
|
||||
TAB_COUNT_TRACKING: 'app:tab_count_tracking',
|
||||
|
||||
// Node Search Analytics
|
||||
NODE_SEARCH: 'app:node_search',
|
||||
NODE_SEARCH_RESULT_SELECTED: 'app:node_search_result_selected',
|
||||
|
||||
// Template Filter Analytics
|
||||
TEMPLATE_FILTER_CHANGED: 'app:template_filter_changed',
|
||||
|
||||
// Execution Lifecycle
|
||||
EXECUTION_START: 'execution_start',
|
||||
@@ -163,3 +277,11 @@ export type TelemetryEventProperties =
|
||||
| RunButtonProperties
|
||||
| ExecutionErrorMetadata
|
||||
| ExecutionSuccessMetadata
|
||||
| CreditTopupMetadata
|
||||
| WorkflowImportMetadata
|
||||
| TemplateLibraryMetadata
|
||||
| PageVisibilityMetadata
|
||||
| TabCountMetadata
|
||||
| NodeSearchMetadata
|
||||
| NodeSearchResultMetadata
|
||||
| TemplateFilterMetadata
|
||||
|
||||
@@ -0,0 +1,683 @@
|
||||
/**
|
||||
* Unit tests for survey normalization utilities
|
||||
* Uses real example data from migration script to verify categorization accuracy
|
||||
*/
|
||||
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import {
|
||||
normalizeIndustry,
|
||||
normalizeUseCase,
|
||||
normalizeSurveyResponses
|
||||
} from '../surveyNormalization'
|
||||
|
||||
describe('normalizeIndustry', () => {
|
||||
describe('Film / TV / Animation category', () => {
|
||||
it('should categorize film and television production', () => {
|
||||
expect(normalizeIndustry('Film and television production')).toBe(
|
||||
'Film / TV / Animation'
|
||||
)
|
||||
expect(normalizeIndustry('film')).toBe('Film / TV / Animation')
|
||||
expect(normalizeIndustry('TV production')).toBe('Film / TV / Animation')
|
||||
expect(normalizeIndustry('animation studio')).toBe(
|
||||
'Film / TV / Animation'
|
||||
)
|
||||
expect(normalizeIndustry('VFX artist')).toBe('Film / TV / Animation')
|
||||
expect(normalizeIndustry('cinema')).toBe('Film / TV / Animation')
|
||||
expect(normalizeIndustry('documentary filmmaker')).toBe(
|
||||
'Film / TV / Animation'
|
||||
)
|
||||
})
|
||||
|
||||
it('should handle typos and variations', () => {
|
||||
expect(normalizeIndustry('animtion')).toBe('Film / TV / Animation')
|
||||
expect(normalizeIndustry('film prod')).toBe('Film / TV / Animation')
|
||||
expect(normalizeIndustry('movie production')).toBe(
|
||||
'Film / TV / Animation'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Marketing / Advertising / Social Media category', () => {
|
||||
it('should categorize marketing and social media', () => {
|
||||
expect(normalizeIndustry('Marketing & Social Media')).toBe(
|
||||
'Marketing / Advertising / Social Media'
|
||||
)
|
||||
expect(normalizeIndustry('digital marketing')).toBe(
|
||||
'Marketing / Advertising / Social Media'
|
||||
)
|
||||
expect(normalizeIndustry('YouTube content creation')).toBe(
|
||||
'Marketing / Advertising / Social Media'
|
||||
)
|
||||
expect(normalizeIndustry('TikTok marketing')).toBe(
|
||||
'Marketing / Advertising / Social Media'
|
||||
)
|
||||
expect(normalizeIndustry('brand promotion')).toBe(
|
||||
'Marketing / Advertising / Social Media'
|
||||
)
|
||||
expect(normalizeIndustry('influencer marketing')).toBe(
|
||||
'Marketing / Advertising / Social Media'
|
||||
)
|
||||
})
|
||||
|
||||
it('should handle social media variations', () => {
|
||||
expect(normalizeIndustry('social content')).toBe(
|
||||
'Marketing / Advertising / Social Media'
|
||||
)
|
||||
expect(normalizeIndustry('content creation')).toBe(
|
||||
'Marketing / Advertising / Social Media'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Software / IT / AI category', () => {
|
||||
it('should categorize software development', () => {
|
||||
expect(normalizeIndustry('Software Development')).toBe(
|
||||
'Software / IT / AI'
|
||||
)
|
||||
expect(normalizeIndustry('tech startup')).toBe('Software / IT / AI')
|
||||
expect(normalizeIndustry('AI research')).toBe('Software / IT / AI')
|
||||
expect(normalizeIndustry('web development')).toBe('Software / IT / AI')
|
||||
expect(normalizeIndustry('machine learning')).toBe('Software / IT / AI')
|
||||
expect(normalizeIndustry('data science')).toBe('Software / IT / AI')
|
||||
expect(normalizeIndustry('programming')).toBe('Software / IT / AI')
|
||||
})
|
||||
|
||||
it('should handle IT variations', () => {
|
||||
expect(normalizeIndustry('software engineer')).toBe('Software / IT / AI')
|
||||
expect(normalizeIndustry('app developer')).toBe('Software / IT / AI')
|
||||
})
|
||||
|
||||
it('should categorize corporate AI research', () => {
|
||||
expect(normalizeIndustry('corporate AI research')).toBe(
|
||||
'Software / IT / AI'
|
||||
)
|
||||
expect(normalizeIndustry('AI research lab')).toBe('Software / IT / AI')
|
||||
expect(normalizeIndustry('tech company AI research')).toBe(
|
||||
'Software / IT / AI'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Gaming / Interactive Media category', () => {
|
||||
it('should categorize gaming industry', () => {
|
||||
expect(normalizeIndustry('Indie Game Studio')).toBe(
|
||||
'Gaming / Interactive Media'
|
||||
)
|
||||
expect(normalizeIndustry('game development')).toBe(
|
||||
'Gaming / Interactive Media'
|
||||
)
|
||||
expect(normalizeIndustry('VR development')).toBe(
|
||||
'Gaming / Interactive Media'
|
||||
)
|
||||
expect(normalizeIndustry('interactive media')).toBe(
|
||||
'Gaming / Interactive Media'
|
||||
)
|
||||
expect(normalizeIndustry('metaverse')).toBe('Gaming / Interactive Media')
|
||||
expect(normalizeIndustry('Unity developer')).toBe(
|
||||
'Gaming / Interactive Media'
|
||||
)
|
||||
})
|
||||
|
||||
it('should handle game dev variations', () => {
|
||||
expect(normalizeIndustry('game dev')).toBe('Gaming / Interactive Media')
|
||||
expect(normalizeIndustry('indie games')).toBe(
|
||||
'Gaming / Interactive Media'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Architecture / Engineering / Construction category', () => {
|
||||
it('should categorize architecture and construction', () => {
|
||||
expect(normalizeIndustry('Architecture firm')).toBe(
|
||||
'Architecture / Engineering / Construction'
|
||||
)
|
||||
expect(normalizeIndustry('construction')).toBe(
|
||||
'Architecture / Engineering / Construction'
|
||||
)
|
||||
expect(normalizeIndustry('civil engineering')).toBe(
|
||||
'Architecture / Engineering / Construction'
|
||||
)
|
||||
expect(normalizeIndustry('interior design')).toBe(
|
||||
'Architecture / Engineering / Construction'
|
||||
)
|
||||
expect(normalizeIndustry('landscape architecture')).toBe(
|
||||
'Architecture / Engineering / Construction'
|
||||
)
|
||||
expect(normalizeIndustry('real estate')).toBe(
|
||||
'Architecture / Engineering / Construction'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Fashion / Beauty / Retail category', () => {
|
||||
it('should categorize fashion and beauty', () => {
|
||||
expect(normalizeIndustry('Custom Jewelry Design')).toBe(
|
||||
'Fashion / Beauty / Retail'
|
||||
)
|
||||
expect(normalizeIndustry('fashion design')).toBe(
|
||||
'Fashion / Beauty / Retail'
|
||||
)
|
||||
expect(normalizeIndustry('beauty industry')).toBe(
|
||||
'Fashion / Beauty / Retail'
|
||||
)
|
||||
expect(normalizeIndustry('retail store')).toBe(
|
||||
'Fashion / Beauty / Retail'
|
||||
)
|
||||
expect(normalizeIndustry('cosmetics')).toBe('Fashion / Beauty / Retail')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Healthcare / Medical / Life Science category', () => {
|
||||
it('should categorize medical and health fields', () => {
|
||||
expect(normalizeIndustry('Medical Research')).toBe(
|
||||
'Healthcare / Medical / Life Science'
|
||||
)
|
||||
expect(normalizeIndustry('healthcare')).toBe(
|
||||
'Healthcare / Medical / Life Science'
|
||||
)
|
||||
expect(normalizeIndustry('biotech')).toBe(
|
||||
'Healthcare / Medical / Life Science'
|
||||
)
|
||||
expect(normalizeIndustry('pharmaceutical')).toBe(
|
||||
'Healthcare / Medical / Life Science'
|
||||
)
|
||||
expect(normalizeIndustry('clinical research')).toBe(
|
||||
'Healthcare / Medical / Life Science'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Education / Research category', () => {
|
||||
it('should categorize education and research', () => {
|
||||
expect(normalizeIndustry('university research')).toBe(
|
||||
'Education / Research'
|
||||
)
|
||||
expect(normalizeIndustry('academic')).toBe('Education / Research')
|
||||
expect(normalizeIndustry('teaching')).toBe('Education / Research')
|
||||
expect(normalizeIndustry('student')).toBe('Education / Research')
|
||||
expect(normalizeIndustry('professor')).toBe('Education / Research')
|
||||
})
|
||||
|
||||
it('should categorize academic AI research', () => {
|
||||
expect(normalizeIndustry('academic AI research')).toBe(
|
||||
'Education / Research'
|
||||
)
|
||||
expect(normalizeIndustry('university AI research')).toBe(
|
||||
'Education / Research'
|
||||
)
|
||||
expect(normalizeIndustry('AI research at university')).toBe(
|
||||
'Education / Research'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Fine Art / Contemporary Art category', () => {
|
||||
it('should categorize art fields', () => {
|
||||
expect(normalizeIndustry('fine art')).toBe('Fine Art / Contemporary Art')
|
||||
expect(normalizeIndustry('contemporary artist')).toBe(
|
||||
'Fine Art / Contemporary Art'
|
||||
)
|
||||
expect(normalizeIndustry('digital art')).toBe(
|
||||
'Fine Art / Contemporary Art'
|
||||
)
|
||||
expect(normalizeIndustry('illustration')).toBe(
|
||||
'Fine Art / Contemporary Art'
|
||||
)
|
||||
expect(normalizeIndustry('gallery')).toBe('Fine Art / Contemporary Art')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Photography / Videography category', () => {
|
||||
it('should categorize photography fields', () => {
|
||||
expect(normalizeIndustry('photography')).toBe('Photography / Videography')
|
||||
expect(normalizeIndustry('wedding photography')).toBe(
|
||||
'Photography / Videography'
|
||||
)
|
||||
expect(normalizeIndustry('commercial photo')).toBe(
|
||||
'Photography / Videography'
|
||||
)
|
||||
expect(normalizeIndustry('videography')).toBe('Photography / Videography')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Product & Industrial Design category', () => {
|
||||
it('should categorize product design', () => {
|
||||
expect(normalizeIndustry('product design')).toBe(
|
||||
'Product & Industrial Design'
|
||||
)
|
||||
expect(normalizeIndustry('industrial design')).toBe(
|
||||
'Product & Industrial Design'
|
||||
)
|
||||
expect(normalizeIndustry('manufacturing')).toBe(
|
||||
'Product & Industrial Design'
|
||||
)
|
||||
expect(normalizeIndustry('3d rendering')).toBe(
|
||||
'Product & Industrial Design'
|
||||
)
|
||||
expect(normalizeIndustry('automotive design')).toBe(
|
||||
'Product & Industrial Design'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Music / Performing Arts category', () => {
|
||||
it('should categorize music and performing arts', () => {
|
||||
expect(normalizeIndustry('music production')).toBe(
|
||||
'Music / Performing Arts'
|
||||
)
|
||||
expect(normalizeIndustry('theater')).toBe('Music / Performing Arts')
|
||||
expect(normalizeIndustry('concert production')).toBe(
|
||||
'Music / Performing Arts'
|
||||
)
|
||||
expect(normalizeIndustry('live events')).toBe('Music / Performing Arts')
|
||||
})
|
||||
})
|
||||
|
||||
describe('E-commerce / Print-on-Demand / Business category', () => {
|
||||
it('should categorize business fields', () => {
|
||||
expect(normalizeIndustry('ecommerce')).toBe(
|
||||
'E-commerce / Print-on-Demand / Business'
|
||||
)
|
||||
expect(normalizeIndustry('print on demand')).toBe(
|
||||
'E-commerce / Print-on-Demand / Business'
|
||||
)
|
||||
expect(normalizeIndustry('startup')).toBe(
|
||||
'E-commerce / Print-on-Demand / Business'
|
||||
)
|
||||
expect(normalizeIndustry('online store')).toBe(
|
||||
'E-commerce / Print-on-Demand / Business'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Nonprofit / Government / Public Sector category', () => {
|
||||
it('should categorize nonprofit and government', () => {
|
||||
expect(normalizeIndustry('nonprofit')).toBe(
|
||||
'Nonprofit / Government / Public Sector'
|
||||
)
|
||||
expect(normalizeIndustry('government agency')).toBe(
|
||||
'Nonprofit / Government / Public Sector'
|
||||
)
|
||||
expect(normalizeIndustry('public service')).toBe(
|
||||
'Nonprofit / Government / Public Sector'
|
||||
)
|
||||
expect(normalizeIndustry('charity')).toBe(
|
||||
'Nonprofit / Government / Public Sector'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Adult / NSFW category', () => {
|
||||
it('should categorize adult content', () => {
|
||||
expect(normalizeIndustry('adult entertainment')).toBe('Adult / NSFW')
|
||||
expect(normalizeIndustry('NSFW content')).toBe('Adult / NSFW')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Other / Undefined category', () => {
|
||||
it('should handle undefined responses', () => {
|
||||
expect(normalizeIndustry('other')).toBe('Other / Undefined')
|
||||
expect(normalizeIndustry('none')).toBe('Other / Undefined')
|
||||
expect(normalizeIndustry('undefined')).toBe('Other / Undefined')
|
||||
expect(normalizeIndustry('unknown')).toBe('Other / Undefined')
|
||||
expect(normalizeIndustry('n/a')).toBe('Other / Undefined')
|
||||
expect(normalizeIndustry('not applicable')).toBe('Other / Undefined')
|
||||
expect(normalizeIndustry('-')).toBe('Other / Undefined')
|
||||
expect(normalizeIndustry('')).toBe('Other / Undefined')
|
||||
})
|
||||
|
||||
it('should handle null and invalid inputs', () => {
|
||||
expect(normalizeIndustry(null as any)).toBe('Other / Undefined')
|
||||
expect(normalizeIndustry(undefined as any)).toBe('Other / Undefined')
|
||||
expect(normalizeIndustry(123 as any)).toBe('Other / Undefined')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Uncategorized responses', () => {
|
||||
it('should preserve unknown creative fields with prefix', () => {
|
||||
expect(normalizeIndustry('Unknown Creative Field')).toBe(
|
||||
'Uncategorized: Unknown Creative Field'
|
||||
)
|
||||
expect(normalizeIndustry('Completely Novel Field')).toBe(
|
||||
'Uncategorized: Completely Novel Field'
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('normalizeUseCase', () => {
|
||||
describe('Content Creation & Marketing', () => {
|
||||
it('should categorize content creation', () => {
|
||||
expect(normalizeUseCase('YouTube thumbnail generation')).toBe(
|
||||
'Content Creation & Marketing'
|
||||
)
|
||||
expect(normalizeUseCase('social media content')).toBe(
|
||||
'Content Creation & Marketing'
|
||||
)
|
||||
expect(normalizeUseCase('marketing campaigns')).toBe(
|
||||
'Content Creation & Marketing'
|
||||
)
|
||||
expect(normalizeUseCase('TikTok content')).toBe(
|
||||
'Content Creation & Marketing'
|
||||
)
|
||||
expect(normalizeUseCase('brand content creation')).toBe(
|
||||
'Content Creation & Marketing'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Art & Illustration', () => {
|
||||
it('should categorize art and illustration', () => {
|
||||
expect(normalizeUseCase('Creating concept art for movies')).toBe(
|
||||
'Art & Illustration'
|
||||
)
|
||||
expect(normalizeUseCase('digital art')).toBe('Art & Illustration')
|
||||
expect(normalizeUseCase('character design')).toBe('Art & Illustration')
|
||||
expect(normalizeUseCase('illustration work')).toBe('Art & Illustration')
|
||||
expect(normalizeUseCase('fantasy art')).toBe('Art & Illustration')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Product Visualization & Design', () => {
|
||||
it('should categorize product work', () => {
|
||||
expect(normalizeUseCase('Product mockup creation')).toBe(
|
||||
'Product Visualization & Design'
|
||||
)
|
||||
expect(normalizeUseCase('3d product rendering')).toBe(
|
||||
'Product Visualization & Design'
|
||||
)
|
||||
expect(normalizeUseCase('prototype visualization')).toBe(
|
||||
'Product Visualization & Design'
|
||||
)
|
||||
expect(normalizeUseCase('industrial design')).toBe(
|
||||
'Product Visualization & Design'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Gaming & Interactive Media', () => {
|
||||
it('should categorize gaming use cases', () => {
|
||||
expect(normalizeUseCase('Game asset generation')).toBe(
|
||||
'Gaming & Interactive Media'
|
||||
)
|
||||
expect(normalizeUseCase('game development')).toBe(
|
||||
'Gaming & Interactive Media'
|
||||
)
|
||||
expect(normalizeUseCase('VR content creation')).toBe(
|
||||
'Gaming & Interactive Media'
|
||||
)
|
||||
expect(normalizeUseCase('interactive media')).toBe(
|
||||
'Gaming & Interactive Media'
|
||||
)
|
||||
expect(normalizeUseCase('game textures')).toBe(
|
||||
'Gaming & Interactive Media'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Architecture & Construction', () => {
|
||||
it('should categorize architecture use cases', () => {
|
||||
expect(normalizeUseCase('Building visualization')).toBe(
|
||||
'Architecture & Construction'
|
||||
)
|
||||
expect(normalizeUseCase('architectural rendering')).toBe(
|
||||
'Architecture & Construction'
|
||||
)
|
||||
expect(normalizeUseCase('interior design mockups')).toBe(
|
||||
'Architecture & Construction'
|
||||
)
|
||||
expect(normalizeUseCase('real estate visualization')).toBe(
|
||||
'Architecture & Construction'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Photography & Image Processing', () => {
|
||||
it('should categorize photography work', () => {
|
||||
expect(normalizeUseCase('Product photography')).toBe(
|
||||
'Photography & Image Processing'
|
||||
)
|
||||
expect(normalizeUseCase('photo editing')).toBe(
|
||||
'Photography & Image Processing'
|
||||
)
|
||||
expect(normalizeUseCase('image enhancement')).toBe(
|
||||
'Photography & Image Processing'
|
||||
)
|
||||
expect(normalizeUseCase('portrait photography')).toBe(
|
||||
'Photography & Image Processing'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Research & Development', () => {
|
||||
it('should categorize research work', () => {
|
||||
expect(normalizeUseCase('Scientific visualization')).toBe(
|
||||
'Research & Development'
|
||||
)
|
||||
expect(normalizeUseCase('research experiments')).toBe(
|
||||
'Research & Development'
|
||||
)
|
||||
expect(normalizeUseCase('prototype testing')).toBe(
|
||||
'Research & Development'
|
||||
)
|
||||
expect(normalizeUseCase('innovation projects')).toBe(
|
||||
'Research & Development'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Personal & Hobby', () => {
|
||||
it('should categorize personal projects', () => {
|
||||
expect(normalizeUseCase('Personal art projects')).toBe('Personal & Hobby')
|
||||
expect(normalizeUseCase('hobby work')).toBe('Personal & Hobby')
|
||||
expect(normalizeUseCase('creative exploration')).toBe('Personal & Hobby')
|
||||
expect(normalizeUseCase('fun experiments')).toBe('Personal & Hobby')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Film & Video Production', () => {
|
||||
it('should categorize film work', () => {
|
||||
expect(normalizeUseCase('movie production')).toBe(
|
||||
'Film & Video Production'
|
||||
)
|
||||
expect(normalizeUseCase('video editing')).toBe('Film & Video Production')
|
||||
expect(normalizeUseCase('visual effects')).toBe('Film & Video Production')
|
||||
expect(normalizeUseCase('storyboard creation')).toBe(
|
||||
'Film & Video Production'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Education & Training', () => {
|
||||
it('should categorize educational use cases', () => {
|
||||
expect(normalizeUseCase('educational content')).toBe(
|
||||
'Education & Training'
|
||||
)
|
||||
expect(normalizeUseCase('training materials')).toBe(
|
||||
'Education & Training'
|
||||
)
|
||||
expect(normalizeUseCase('tutorial creation')).toBe('Education & Training')
|
||||
expect(normalizeUseCase('academic projects')).toBe('Education & Training')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Other / Undefined category', () => {
|
||||
it('should handle undefined responses', () => {
|
||||
expect(normalizeUseCase('other')).toBe('Other / Undefined')
|
||||
expect(normalizeUseCase('none')).toBe('Other / Undefined')
|
||||
expect(normalizeUseCase('undefined')).toBe('Other / Undefined')
|
||||
expect(normalizeUseCase('')).toBe('Other / Undefined')
|
||||
expect(normalizeUseCase(null as any)).toBe('Other / Undefined')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Uncategorized responses', () => {
|
||||
it('should preserve unknown use cases with prefix', () => {
|
||||
expect(normalizeUseCase('Mysterious Use Case')).toBe(
|
||||
'Uncategorized: Mysterious Use Case'
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('normalizeSurveyResponses', () => {
|
||||
it('should normalize both industry and use case', () => {
|
||||
const input = {
|
||||
industry: 'Film and television production',
|
||||
useCase: 'Creating concept art for movies',
|
||||
familiarity: 'Expert'
|
||||
}
|
||||
|
||||
const result = normalizeSurveyResponses(input)
|
||||
|
||||
expect(result).toEqual({
|
||||
industry: 'Film and television production',
|
||||
industry_normalized: 'Film / TV / Animation',
|
||||
industry_raw: 'Film and television production',
|
||||
useCase: 'Creating concept art for movies',
|
||||
useCase_normalized: 'Art & Illustration',
|
||||
useCase_raw: 'Creating concept art for movies',
|
||||
familiarity: 'Expert'
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle partial responses', () => {
|
||||
const input = {
|
||||
industry: 'Software Development',
|
||||
familiarity: 'Beginner'
|
||||
}
|
||||
|
||||
const result = normalizeSurveyResponses(input)
|
||||
|
||||
expect(result).toEqual({
|
||||
industry: 'Software Development',
|
||||
industry_normalized: 'Software / IT / AI',
|
||||
industry_raw: 'Software Development',
|
||||
familiarity: 'Beginner'
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle empty responses', () => {
|
||||
const input = {
|
||||
familiarity: 'Intermediate'
|
||||
}
|
||||
|
||||
const result = normalizeSurveyResponses(input)
|
||||
|
||||
expect(result).toEqual({
|
||||
familiarity: 'Intermediate'
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle uncategorized responses', () => {
|
||||
const input = {
|
||||
industry: 'Unknown Creative Field',
|
||||
useCase: 'Mysterious Use Case'
|
||||
}
|
||||
|
||||
const result = normalizeSurveyResponses(input)
|
||||
|
||||
expect(result).toEqual({
|
||||
industry: 'Unknown Creative Field',
|
||||
industry_normalized: 'Uncategorized: Unknown Creative Field',
|
||||
industry_raw: 'Unknown Creative Field',
|
||||
useCase: 'Mysterious Use Case',
|
||||
useCase_normalized: 'Uncategorized: Mysterious Use Case',
|
||||
useCase_raw: 'Mysterious Use Case'
|
||||
})
|
||||
})
|
||||
|
||||
describe('Migration script example data validation', () => {
|
||||
it('should correctly categorize all migration script examples', () => {
|
||||
const examples = [
|
||||
{
|
||||
input: {
|
||||
industry: 'Film and television production',
|
||||
useCase: 'Creating concept art for movies'
|
||||
},
|
||||
expected: {
|
||||
industry: 'Film / TV / Animation',
|
||||
useCase: 'Art & Illustration'
|
||||
}
|
||||
},
|
||||
{
|
||||
input: {
|
||||
industry: 'Marketing & Social Media',
|
||||
useCase: 'YouTube thumbnail generation'
|
||||
},
|
||||
expected: {
|
||||
industry: 'Marketing / Advertising / Social Media',
|
||||
useCase: 'Content Creation & Marketing'
|
||||
}
|
||||
},
|
||||
{
|
||||
input: {
|
||||
industry: 'Software Development',
|
||||
useCase: 'Product mockup creation'
|
||||
},
|
||||
expected: {
|
||||
industry: 'Software / IT / AI',
|
||||
useCase: 'Product Visualization & Design'
|
||||
}
|
||||
},
|
||||
{
|
||||
input: {
|
||||
industry: 'Indie Game Studio',
|
||||
useCase: 'Game asset generation'
|
||||
},
|
||||
expected: {
|
||||
industry: 'Gaming / Interactive Media',
|
||||
useCase: 'Gaming & Interactive Media'
|
||||
}
|
||||
},
|
||||
{
|
||||
input: {
|
||||
industry: 'Architecture firm',
|
||||
useCase: 'Building visualization'
|
||||
},
|
||||
expected: {
|
||||
industry: 'Architecture / Engineering / Construction',
|
||||
useCase: 'Architecture & Construction'
|
||||
}
|
||||
},
|
||||
{
|
||||
input: {
|
||||
industry: 'Custom Jewelry Design',
|
||||
useCase: 'Product photography'
|
||||
},
|
||||
expected: {
|
||||
industry: 'Fashion / Beauty / Retail',
|
||||
useCase: 'Photography & Image Processing'
|
||||
}
|
||||
},
|
||||
{
|
||||
input: {
|
||||
industry: 'Medical Research',
|
||||
useCase: 'Scientific visualization'
|
||||
},
|
||||
expected: {
|
||||
industry: 'Healthcare / Medical / Life Science',
|
||||
useCase: 'Research & Development'
|
||||
}
|
||||
},
|
||||
{
|
||||
input: {
|
||||
industry: 'Unknown Creative Field',
|
||||
useCase: 'Personal art projects'
|
||||
},
|
||||
expected: {
|
||||
industry: 'Uncategorized: Unknown Creative Field',
|
||||
useCase: 'Personal & Hobby'
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
examples.forEach(({ input, expected }) => {
|
||||
const result = normalizeSurveyResponses(input)
|
||||
expect(result.industry_normalized).toBe(expected.industry)
|
||||
expect(result.useCase_normalized).toBe(expected.useCase)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
606
src/platform/telemetry/utils/surveyNormalization.ts
Normal file
606
src/platform/telemetry/utils/surveyNormalization.ts
Normal file
@@ -0,0 +1,606 @@
|
||||
/**
|
||||
* Survey Response Normalization Utilities
|
||||
*
|
||||
* Smart categorization system to normalize free-text survey responses
|
||||
* into standardized categories for better analytics breakdowns.
|
||||
* Uses Fuse.js for fuzzy matching against category keywords.
|
||||
*/
|
||||
import Fuse from 'fuse.js'
|
||||
|
||||
interface CategoryMapping {
|
||||
name: string
|
||||
keywords: string[]
|
||||
userCount?: number // For reference from analysis
|
||||
}
|
||||
|
||||
/**
|
||||
* Industry category mappings based on ~9,000 user analysis
|
||||
*/
|
||||
const INDUSTRY_CATEGORIES: CategoryMapping[] = [
|
||||
{
|
||||
name: 'Film / TV / Animation',
|
||||
userCount: 2885,
|
||||
keywords: [
|
||||
'film',
|
||||
'tv',
|
||||
'television',
|
||||
'animation',
|
||||
'animation studio',
|
||||
'tv production',
|
||||
'film production',
|
||||
'story',
|
||||
'anime',
|
||||
'video',
|
||||
'cinematography',
|
||||
'visual effects',
|
||||
'vfx',
|
||||
'vfx artist',
|
||||
'movie',
|
||||
'cinema',
|
||||
'documentary',
|
||||
'documentary filmmaker',
|
||||
'broadcast',
|
||||
'streaming',
|
||||
'production',
|
||||
'director',
|
||||
'filmmaker',
|
||||
'post-production',
|
||||
'editing'
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'Marketing / Advertising / Social Media',
|
||||
userCount: 1340,
|
||||
keywords: [
|
||||
'marketing',
|
||||
'advertising',
|
||||
'youtube',
|
||||
'tiktok',
|
||||
'social media',
|
||||
'content creation',
|
||||
'influencer',
|
||||
'brand',
|
||||
'promotion',
|
||||
'digital marketing',
|
||||
'seo',
|
||||
'campaigns',
|
||||
'copywriting',
|
||||
'growth',
|
||||
'engagement'
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'Software / IT / AI',
|
||||
userCount: 1100,
|
||||
keywords: [
|
||||
'software',
|
||||
'software development',
|
||||
'software engineer',
|
||||
'it',
|
||||
'ai',
|
||||
'ai research',
|
||||
'corporate ai research',
|
||||
'ai research lab',
|
||||
'tech company ai research',
|
||||
'developer',
|
||||
'app developer',
|
||||
'consulting',
|
||||
'tech',
|
||||
'tech startup',
|
||||
'programmer',
|
||||
'data science',
|
||||
'machine learning',
|
||||
'coding',
|
||||
'programming',
|
||||
'web development',
|
||||
'app development',
|
||||
'saas'
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'Product & Industrial Design',
|
||||
userCount: 1050,
|
||||
keywords: [
|
||||
'product design',
|
||||
'industrial',
|
||||
'manufacturing',
|
||||
'3d rendering',
|
||||
'product visualization',
|
||||
'mechanical',
|
||||
'automotive',
|
||||
'cad',
|
||||
'prototype',
|
||||
'design engineering',
|
||||
'invention'
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'Fine Art / Contemporary Art',
|
||||
userCount: 780,
|
||||
keywords: [
|
||||
'fine art',
|
||||
'art',
|
||||
'illustration',
|
||||
'contemporary',
|
||||
'artist',
|
||||
'painting',
|
||||
'drawing',
|
||||
'sculpture',
|
||||
'gallery',
|
||||
'canvas',
|
||||
'digital art',
|
||||
'mixed media',
|
||||
'abstract',
|
||||
'portrait'
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'Education / Research',
|
||||
userCount: 640,
|
||||
keywords: [
|
||||
'education',
|
||||
'student',
|
||||
'teacher',
|
||||
'research',
|
||||
'university research',
|
||||
'academic ai research',
|
||||
'university ai research',
|
||||
'ai research at university',
|
||||
'learning',
|
||||
'university',
|
||||
'school',
|
||||
'academic',
|
||||
'professor',
|
||||
'curriculum',
|
||||
'training',
|
||||
'instruction',
|
||||
'pedagogy'
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'Architecture / Engineering / Construction',
|
||||
userCount: 420,
|
||||
keywords: [
|
||||
'architecture',
|
||||
'architecture firm',
|
||||
'construction',
|
||||
'engineering',
|
||||
'civil',
|
||||
'civil engineering',
|
||||
'cad',
|
||||
'building',
|
||||
'structural',
|
||||
'landscape',
|
||||
'landscape architecture',
|
||||
'interior design',
|
||||
'real estate',
|
||||
'planning',
|
||||
'blueprints'
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'Gaming / Interactive Media',
|
||||
userCount: 410,
|
||||
keywords: [
|
||||
'gaming',
|
||||
'game dev',
|
||||
'game development',
|
||||
'indie game studio',
|
||||
'vr development',
|
||||
'roblox',
|
||||
'interactive',
|
||||
'interactive media',
|
||||
'virtual world',
|
||||
'vr',
|
||||
'ar',
|
||||
'metaverse',
|
||||
'simulation',
|
||||
'unity',
|
||||
'unity developer',
|
||||
'unreal',
|
||||
'indie games'
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'Photography / Videography',
|
||||
userCount: 70,
|
||||
keywords: [
|
||||
'photography',
|
||||
'photo',
|
||||
'videography',
|
||||
'camera',
|
||||
'image',
|
||||
'portrait',
|
||||
'wedding',
|
||||
'commercial photo',
|
||||
'stock photography',
|
||||
'photojournalism',
|
||||
'event photography'
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'Fashion / Beauty / Retail',
|
||||
userCount: 25,
|
||||
keywords: [
|
||||
'fashion',
|
||||
'fashion design',
|
||||
'beauty',
|
||||
'beauty industry',
|
||||
'jewelry',
|
||||
'jewelry design',
|
||||
'custom jewelry design',
|
||||
'retail',
|
||||
'retail store',
|
||||
'style',
|
||||
'clothing',
|
||||
'cosmetics',
|
||||
'makeup',
|
||||
'accessories',
|
||||
'boutique'
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'Music / Performing Arts',
|
||||
userCount: 25,
|
||||
keywords: [
|
||||
'music',
|
||||
'music production',
|
||||
'vj',
|
||||
'dance',
|
||||
'projection mapping',
|
||||
'audio visual',
|
||||
'concert',
|
||||
'concert production',
|
||||
'performance',
|
||||
'theater',
|
||||
'stage',
|
||||
'live events'
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'Healthcare / Medical / Life Science',
|
||||
userCount: 30,
|
||||
keywords: [
|
||||
'healthcare',
|
||||
'medical',
|
||||
'medical research',
|
||||
'doctor',
|
||||
'biotech',
|
||||
'life science',
|
||||
'pharmaceutical',
|
||||
'clinical',
|
||||
'clinical research',
|
||||
'hospital',
|
||||
'medicine',
|
||||
'health'
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'E-commerce / Print-on-Demand / Business',
|
||||
userCount: 15,
|
||||
keywords: [
|
||||
'ecommerce',
|
||||
'e-commerce',
|
||||
'print on demand',
|
||||
'shop',
|
||||
'business',
|
||||
'commercial',
|
||||
'startup',
|
||||
'entrepreneur',
|
||||
'sales',
|
||||
'online store'
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'Nonprofit / Government / Public Sector',
|
||||
userCount: 15,
|
||||
keywords: [
|
||||
'501c3',
|
||||
'ngo',
|
||||
'government',
|
||||
'public service',
|
||||
'policy',
|
||||
'nonprofit',
|
||||
'charity',
|
||||
'civic',
|
||||
'community',
|
||||
'social impact'
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'Adult / NSFW',
|
||||
userCount: 10,
|
||||
keywords: [
|
||||
'nsfw',
|
||||
'nsfw content',
|
||||
'adult',
|
||||
'adult entertainment',
|
||||
'erotic',
|
||||
'explicit',
|
||||
'xxx',
|
||||
'porn'
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
/**
|
||||
* Use case category mappings based on common patterns
|
||||
*/
|
||||
const USE_CASE_CATEGORIES: CategoryMapping[] = [
|
||||
{
|
||||
name: 'Content Creation & Marketing',
|
||||
keywords: [
|
||||
'content creation',
|
||||
'social media',
|
||||
'marketing',
|
||||
'marketing campaigns',
|
||||
'advertising',
|
||||
'youtube',
|
||||
'youtube thumbnail',
|
||||
'youtube thumbnail generation',
|
||||
'tiktok',
|
||||
'instagram',
|
||||
'thumbnails',
|
||||
'posts',
|
||||
'campaigns',
|
||||
'brand content'
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'Art & Illustration',
|
||||
keywords: [
|
||||
'art',
|
||||
'illustration',
|
||||
'drawing',
|
||||
'painting',
|
||||
'concept art',
|
||||
'creating concept art',
|
||||
'character design',
|
||||
'digital art',
|
||||
'fantasy art',
|
||||
'portraits'
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'Product Visualization & Design',
|
||||
keywords: [
|
||||
'product',
|
||||
'product mockup',
|
||||
'product mockup creation',
|
||||
'visualization',
|
||||
'prototype visualization',
|
||||
'design',
|
||||
'prototype',
|
||||
'mockup',
|
||||
'3d rendering',
|
||||
'industrial design',
|
||||
'product photos'
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'Film & Video Production',
|
||||
keywords: [
|
||||
'film',
|
||||
'video',
|
||||
'video editing',
|
||||
'movie',
|
||||
'movie production',
|
||||
'animation',
|
||||
'vfx',
|
||||
'visual effects',
|
||||
'storyboard',
|
||||
'storyboard creation',
|
||||
'cinematography',
|
||||
'post production'
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'Gaming & Interactive Media',
|
||||
keywords: [
|
||||
'game',
|
||||
'gaming',
|
||||
'game asset generation',
|
||||
'game assets',
|
||||
'game development',
|
||||
'game textures',
|
||||
'interactive',
|
||||
'vr',
|
||||
'vr content creation',
|
||||
'ar',
|
||||
'virtual',
|
||||
'simulation',
|
||||
'metaverse',
|
||||
'textures'
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'Architecture & Construction',
|
||||
keywords: [
|
||||
'architecture',
|
||||
'architectural rendering',
|
||||
'building',
|
||||
'building visualization',
|
||||
'construction',
|
||||
'interior design',
|
||||
'interior design mockups',
|
||||
'landscape',
|
||||
'real estate',
|
||||
'real estate visualization',
|
||||
'floor plans',
|
||||
'renderings'
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'Education & Training',
|
||||
keywords: [
|
||||
'education',
|
||||
'educational',
|
||||
'educational content',
|
||||
'training',
|
||||
'training materials',
|
||||
'learning',
|
||||
'teaching',
|
||||
'tutorial',
|
||||
'tutorial creation',
|
||||
'course',
|
||||
'academic',
|
||||
'academic projects',
|
||||
'instructional',
|
||||
'workshops'
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'Research & Development',
|
||||
keywords: [
|
||||
'research',
|
||||
'research experiments',
|
||||
'development',
|
||||
'experiment',
|
||||
'prototype',
|
||||
'prototype testing',
|
||||
'testing',
|
||||
'analysis',
|
||||
'study',
|
||||
'innovation',
|
||||
'innovation projects',
|
||||
'r&d',
|
||||
'scientific visualization'
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'Personal & Hobby',
|
||||
keywords: [
|
||||
'personal',
|
||||
'personal art projects',
|
||||
'hobby',
|
||||
'hobby work',
|
||||
'fun',
|
||||
'fun experiments',
|
||||
'experiment',
|
||||
'learning',
|
||||
'curiosity',
|
||||
'explore',
|
||||
'creative',
|
||||
'creative exploration',
|
||||
'side project'
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'Photography & Image Processing',
|
||||
keywords: [
|
||||
'photography',
|
||||
'product photography',
|
||||
'portrait photography',
|
||||
'photo',
|
||||
'photo editing',
|
||||
'image',
|
||||
'image enhancement',
|
||||
'portrait',
|
||||
'editing',
|
||||
'enhancement',
|
||||
'restoration',
|
||||
'photo manipulation'
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
/**
|
||||
* Fuse.js configuration for category matching
|
||||
*/
|
||||
const FUSE_OPTIONS = {
|
||||
keys: ['keywords'],
|
||||
threshold: 0.53, // Higher = more lenient matching
|
||||
minMatchCharLength: 5,
|
||||
includeScore: true,
|
||||
includeMatches: true,
|
||||
ignoreLocation: true,
|
||||
findAllMatches: true
|
||||
}
|
||||
|
||||
/**
|
||||
* Create Fuse instances for category matching
|
||||
*/
|
||||
const industryFuse = new Fuse(INDUSTRY_CATEGORIES, FUSE_OPTIONS)
|
||||
const useCaseFuse = new Fuse(USE_CASE_CATEGORIES, FUSE_OPTIONS)
|
||||
|
||||
/**
|
||||
* Normalize industry responses using Fuse.js fuzzy search
|
||||
*/
|
||||
export function normalizeIndustry(rawIndustry: string): string {
|
||||
if (!rawIndustry || typeof rawIndustry !== 'string') {
|
||||
return 'Other / Undefined'
|
||||
}
|
||||
|
||||
const industry = rawIndustry.toLowerCase().trim()
|
||||
|
||||
// Handle common undefined responses
|
||||
if (
|
||||
industry.match(/^(other|none|undefined|unknown|n\/a|not applicable|-|)$/)
|
||||
) {
|
||||
return 'Other / Undefined'
|
||||
}
|
||||
|
||||
// Fuse.js fuzzy search for best category match
|
||||
const results = industryFuse.search(rawIndustry)
|
||||
|
||||
if (results.length > 0) {
|
||||
return results[0].item.name
|
||||
}
|
||||
|
||||
// No good match found - preserve original with prefix
|
||||
return `Uncategorized: ${rawIndustry}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize use case responses using Fuse.js fuzzy search
|
||||
*/
|
||||
export function normalizeUseCase(rawUseCase: string): string {
|
||||
if (!rawUseCase || typeof rawUseCase !== 'string') {
|
||||
return 'Other / Undefined'
|
||||
}
|
||||
|
||||
const useCase = rawUseCase.toLowerCase().trim()
|
||||
|
||||
// Handle common undefined responses
|
||||
if (
|
||||
useCase.match(/^(other|none|undefined|unknown|n\/a|not applicable|-|)$/)
|
||||
) {
|
||||
return 'Other / Undefined'
|
||||
}
|
||||
|
||||
// Fuse.js fuzzy search for best category match
|
||||
const results = useCaseFuse.search(rawUseCase)
|
||||
|
||||
if (results.length > 0) {
|
||||
return results[0].item.name
|
||||
}
|
||||
|
||||
// No good match found - preserve original with prefix
|
||||
return `Uncategorized: ${rawUseCase}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply normalization to survey responses
|
||||
* Creates both normalized and raw versions of responses
|
||||
*/
|
||||
export function normalizeSurveyResponses(responses: {
|
||||
industry?: string
|
||||
useCase?: string
|
||||
[key: string]: any
|
||||
}) {
|
||||
const normalized = { ...responses }
|
||||
|
||||
// Normalize industry
|
||||
if (responses.industry) {
|
||||
normalized.industry_normalized = normalizeIndustry(responses.industry)
|
||||
normalized.industry_raw = responses.industry
|
||||
}
|
||||
|
||||
// Normalize use case
|
||||
if (responses.useCase) {
|
||||
normalized.useCase_normalized = normalizeUseCase(responses.useCase)
|
||||
normalized.useCase_raw = responses.useCase
|
||||
}
|
||||
|
||||
return normalized
|
||||
}
|
||||
Reference in New Issue
Block a user