Initial commit - WhyRating Engine (Google Reviews Scraper)
This commit is contained in:
150
web/lib/categories.ts
Normal file
150
web/lib/categories.ts
Normal file
@@ -0,0 +1,150 @@
|
||||
// Category types and utilities
|
||||
|
||||
export interface Category {
|
||||
id: number;
|
||||
name: string;
|
||||
slug: string;
|
||||
path: string;
|
||||
level: number;
|
||||
parent_id: number | null;
|
||||
category_count: number;
|
||||
children?: Category[];
|
||||
}
|
||||
|
||||
export interface CategoryTreeNode {
|
||||
id: string;
|
||||
name: string;
|
||||
children?: CategoryTreeNode[];
|
||||
data?: Category;
|
||||
}
|
||||
|
||||
export interface CategoryStats {
|
||||
total: number;
|
||||
sectors: number;
|
||||
business_types: number;
|
||||
sub_categories: number;
|
||||
leaf_categories: number;
|
||||
}
|
||||
|
||||
// Convert flat categories to tree structure
|
||||
export function buildCategoryTree(categories: Category[]): CategoryTreeNode[] {
|
||||
const map = new Map<string, CategoryTreeNode>();
|
||||
const roots: CategoryTreeNode[] = [];
|
||||
|
||||
// First pass: create all nodes
|
||||
for (const cat of categories) {
|
||||
map.set(cat.path, {
|
||||
id: cat.path,
|
||||
name: cat.name,
|
||||
children: [],
|
||||
data: cat,
|
||||
});
|
||||
}
|
||||
|
||||
// Second pass: link children to parents
|
||||
for (const cat of categories) {
|
||||
const node = map.get(cat.path)!;
|
||||
const pathParts = cat.path.split('.');
|
||||
|
||||
if (pathParts.length === 1) {
|
||||
// Root node
|
||||
roots.push(node);
|
||||
} else {
|
||||
// Find parent
|
||||
const parentPath = pathParts.slice(0, -1).join('.');
|
||||
const parent = map.get(parentPath);
|
||||
if (parent) {
|
||||
parent.children!.push(node);
|
||||
} else {
|
||||
// Parent not found, add as root
|
||||
roots.push(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort children alphabetically
|
||||
const sortChildren = (nodes: CategoryTreeNode[]) => {
|
||||
nodes.sort((a, b) => a.name.localeCompare(b.name));
|
||||
for (const node of nodes) {
|
||||
if (node.children && node.children.length > 0) {
|
||||
sortChildren(node.children);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
sortChildren(roots);
|
||||
|
||||
return roots;
|
||||
}
|
||||
|
||||
// Convert to react-d3-tree format
|
||||
export function toD3Tree(nodes: CategoryTreeNode[]): any {
|
||||
return nodes.map((node) => ({
|
||||
name: node.name,
|
||||
attributes: {
|
||||
level: node.data?.level,
|
||||
count: node.data?.category_count,
|
||||
},
|
||||
children: node.children && node.children.length > 0
|
||||
? toD3Tree(node.children)
|
||||
: undefined,
|
||||
}));
|
||||
}
|
||||
|
||||
// Get breadcrumb path for a category
|
||||
export function getCategoryBreadcrumb(path: string, categories: Category[]): Category[] {
|
||||
const parts = path.split('.');
|
||||
const breadcrumb: Category[] = [];
|
||||
|
||||
for (let i = 1; i <= parts.length; i++) {
|
||||
const partialPath = parts.slice(0, i).join('.');
|
||||
const cat = categories.find((c) => c.path === partialPath);
|
||||
if (cat) {
|
||||
breadcrumb.push(cat);
|
||||
}
|
||||
}
|
||||
|
||||
return breadcrumb;
|
||||
}
|
||||
|
||||
// Search categories
|
||||
export function searchCategories(categories: Category[], query: string): Category[] {
|
||||
const lowerQuery = query.toLowerCase();
|
||||
return categories.filter(
|
||||
(cat) =>
|
||||
cat.name.toLowerCase().includes(lowerQuery) ||
|
||||
cat.path.toLowerCase().includes(lowerQuery)
|
||||
);
|
||||
}
|
||||
|
||||
// Get level name
|
||||
export function getLevelName(level: number): string {
|
||||
switch (level) {
|
||||
case 1:
|
||||
return 'Sector';
|
||||
case 2:
|
||||
return 'Business Type';
|
||||
case 3:
|
||||
return 'Sub-category';
|
||||
case 4:
|
||||
return 'Category';
|
||||
default:
|
||||
return 'Unknown';
|
||||
}
|
||||
}
|
||||
|
||||
// Get level color
|
||||
export function getLevelColor(level: number): string {
|
||||
switch (level) {
|
||||
case 1:
|
||||
return 'bg-blue-500';
|
||||
case 2:
|
||||
return 'bg-green-500';
|
||||
case 3:
|
||||
return 'bg-yellow-500';
|
||||
case 4:
|
||||
return 'bg-purple-500';
|
||||
default:
|
||||
return 'bg-gray-500';
|
||||
}
|
||||
}
|
||||
@@ -68,6 +68,7 @@ export async function getWidgetData(
|
||||
widgetId: string,
|
||||
params: {
|
||||
business_id?: string;
|
||||
job_id?: string;
|
||||
time_range?: string;
|
||||
page?: number;
|
||||
page_size?: number;
|
||||
@@ -78,6 +79,9 @@ export async function getWidgetData(
|
||||
if (params.business_id) {
|
||||
searchParams.set('business_id', params.business_id);
|
||||
}
|
||||
if (params.job_id) {
|
||||
searchParams.set('job_id', params.job_id);
|
||||
}
|
||||
if (params.time_range) {
|
||||
searchParams.set('time_range', params.time_range);
|
||||
}
|
||||
|
||||
@@ -72,6 +72,15 @@ export interface PipelineDetail extends PipelineInfo {
|
||||
updated_at?: string;
|
||||
}
|
||||
|
||||
// Per-stage execution metrics
|
||||
export interface StageMetrics {
|
||||
duration_ms: number;
|
||||
success: boolean;
|
||||
records_in: number;
|
||||
records_out: number;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
// Execution status
|
||||
export interface ExecutionStatus {
|
||||
id: string;
|
||||
@@ -87,6 +96,10 @@ export interface ExecutionStatus {
|
||||
started_at?: string;
|
||||
completed_at?: string;
|
||||
created_at?: string;
|
||||
total_duration_ms?: number;
|
||||
input_summary?: Record<string, unknown>;
|
||||
result_summary?: Record<string, unknown>;
|
||||
stage_metrics?: Record<string, StageMetrics>;
|
||||
}
|
||||
|
||||
// Widget-specific data types
|
||||
|
||||
139
web/lib/taxonomy/data.ts
Normal file
139
web/lib/taxonomy/data.ts
Normal file
@@ -0,0 +1,139 @@
|
||||
/**
|
||||
* Static import of URT Taxonomy data
|
||||
*/
|
||||
|
||||
import urtData from './urt-codes.json';
|
||||
import type { URTTaxonomy } from './types';
|
||||
|
||||
export const taxonomy = urtData as URTTaxonomy;
|
||||
|
||||
/**
|
||||
* Get subcode count for a category
|
||||
*/
|
||||
export function getSubcodeCount(categoryKey: string): number {
|
||||
const domainKey = categoryKey[0];
|
||||
const domain = taxonomy.domains[domainKey];
|
||||
if (!domain) return 0;
|
||||
|
||||
const category = domain.categories[categoryKey];
|
||||
if (!category) return 0;
|
||||
|
||||
return Object.keys(category.subcodes).length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get subcode count for a domain
|
||||
*/
|
||||
export function getDomainSubcodeCount(domainKey: string): number {
|
||||
return taxonomy.indices.subcodes_by_domain[domainKey] || 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get category count for a domain
|
||||
*/
|
||||
export function getDomainCategoryCount(domainKey: string): number {
|
||||
const domain = taxonomy.domains[domainKey];
|
||||
if (!domain) return 0;
|
||||
return Object.keys(domain.categories).length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search taxonomy codes by text
|
||||
*/
|
||||
export function searchTaxonomy(query: string): {
|
||||
domains: string[];
|
||||
categories: string[];
|
||||
subcodes: string[];
|
||||
} {
|
||||
const normalizedQuery = query.toLowerCase().trim();
|
||||
if (!normalizedQuery) {
|
||||
return { domains: [], categories: [], subcodes: [] };
|
||||
}
|
||||
|
||||
const matchedDomains: string[] = [];
|
||||
const matchedCategories: string[] = [];
|
||||
const matchedSubcodes: string[] = [];
|
||||
|
||||
for (const [domainKey, domain] of Object.entries(taxonomy.domains)) {
|
||||
const domainMatches =
|
||||
domainKey.toLowerCase().includes(normalizedQuery) ||
|
||||
domain.name.toLowerCase().includes(normalizedQuery) ||
|
||||
domain.description.toLowerCase().includes(normalizedQuery);
|
||||
|
||||
if (domainMatches) {
|
||||
matchedDomains.push(domainKey);
|
||||
}
|
||||
|
||||
for (const [categoryKey, category] of Object.entries(domain.categories)) {
|
||||
const categoryMatches =
|
||||
categoryKey.toLowerCase().includes(normalizedQuery) ||
|
||||
category.name.toLowerCase().includes(normalizedQuery) ||
|
||||
category.definition.toLowerCase().includes(normalizedQuery);
|
||||
|
||||
if (categoryMatches) {
|
||||
matchedCategories.push(categoryKey);
|
||||
}
|
||||
|
||||
for (const [subcodeKey, subcode] of Object.entries(category.subcodes)) {
|
||||
const subcodeMatches =
|
||||
subcodeKey.toLowerCase().includes(normalizedQuery) ||
|
||||
subcode.name.toLowerCase().includes(normalizedQuery) ||
|
||||
subcode.definition.toLowerCase().includes(normalizedQuery) ||
|
||||
subcode.positive_example.toLowerCase().includes(normalizedQuery) ||
|
||||
subcode.negative_example.toLowerCase().includes(normalizedQuery);
|
||||
|
||||
if (subcodeMatches) {
|
||||
matchedSubcodes.push(subcodeKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
domains: matchedDomains,
|
||||
categories: matchedCategories,
|
||||
subcodes: matchedSubcodes,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get parent domain and category for a subcode
|
||||
*/
|
||||
export function getSubcodeContext(subcodeKey: string): {
|
||||
domainKey: string;
|
||||
categoryKey: string;
|
||||
} | null {
|
||||
// Parse subcode key like "O1.01" -> domain "O", category "O1"
|
||||
const match = subcodeKey.match(/^([OPJEAVR])(\d)/);
|
||||
if (!match) return null;
|
||||
|
||||
const domainKey = match[1];
|
||||
const categoryKey = `${match[1]}${match[2]}`;
|
||||
|
||||
return { domainKey, categoryKey };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get parent domain for a category
|
||||
*/
|
||||
export function getCategoryDomain(categoryKey: string): string | null {
|
||||
const match = categoryKey.match(/^([OPJEAVR])/);
|
||||
return match ? match[1] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get subcode definition by subcode key (e.g., "J1.04" -> "Meeting scheduled times")
|
||||
*/
|
||||
export function getSubcodeDefinition(subcodeKey: string): string | null {
|
||||
const context = getSubcodeContext(subcodeKey);
|
||||
if (!context) return null;
|
||||
|
||||
const domain = taxonomy.domains[context.domainKey];
|
||||
if (!domain) return null;
|
||||
|
||||
const category = domain.categories[context.categoryKey];
|
||||
if (!category) return null;
|
||||
|
||||
const subcode = category.subcodes[subcodeKey];
|
||||
return subcode?.definition || null;
|
||||
}
|
||||
177
web/lib/taxonomy/types.ts
Normal file
177
web/lib/taxonomy/types.ts
Normal file
@@ -0,0 +1,177 @@
|
||||
/**
|
||||
* TypeScript types for URT Taxonomy Explorer
|
||||
*/
|
||||
|
||||
// ==================== Subcode Types ====================
|
||||
|
||||
export interface Subcode {
|
||||
name: string;
|
||||
definition: string;
|
||||
positive_example: string;
|
||||
negative_example: string;
|
||||
dont_confuse_with: string;
|
||||
dont_confuse_reason: string;
|
||||
}
|
||||
|
||||
export interface Category {
|
||||
name: string;
|
||||
definition: string;
|
||||
subcodes: Record<string, Subcode>;
|
||||
}
|
||||
|
||||
export interface Domain {
|
||||
name: string;
|
||||
description: string;
|
||||
core_question: string;
|
||||
default_owner: string;
|
||||
categories: Record<string, Category>;
|
||||
}
|
||||
|
||||
// ==================== Causal Code Types ====================
|
||||
|
||||
export interface CausalCode {
|
||||
name: string;
|
||||
definition: string;
|
||||
}
|
||||
|
||||
export interface CausalLayer {
|
||||
layer: string;
|
||||
prefix: string;
|
||||
description: string;
|
||||
codes: Record<string, CausalCode>;
|
||||
}
|
||||
|
||||
// ==================== Metadata Types ====================
|
||||
|
||||
export interface MetadataValue {
|
||||
label: string;
|
||||
definition: string;
|
||||
example?: string;
|
||||
constraint?: string;
|
||||
markers?: string[];
|
||||
trigger_words?: string[];
|
||||
}
|
||||
|
||||
export interface MetadataDimension {
|
||||
code: string;
|
||||
name: string;
|
||||
description: string;
|
||||
default?: string;
|
||||
values: Record<string, MetadataValue>;
|
||||
}
|
||||
|
||||
// ==================== Profile Types ====================
|
||||
|
||||
export interface Profile {
|
||||
name: string;
|
||||
use_case: string;
|
||||
code_tier: number;
|
||||
code_count: number;
|
||||
code_type: string;
|
||||
complexity: string;
|
||||
required_fields: string[];
|
||||
optional_fields: string[];
|
||||
forbidden_fields: string[];
|
||||
primary_code_pattern: string;
|
||||
secondary_codes_allowed: boolean;
|
||||
secondary_codes_max?: number;
|
||||
secondary_codes_tier?: number;
|
||||
}
|
||||
|
||||
// ==================== Statistics ====================
|
||||
|
||||
export interface TaxonomyStatistics {
|
||||
domains: number;
|
||||
categories: number;
|
||||
subcodes_spec_claims: number;
|
||||
subcodes_actual: number;
|
||||
causal_codes: number;
|
||||
metadata_dimensions: number;
|
||||
metadata_values: number;
|
||||
total_classification_codes_spec_claims: number;
|
||||
total_classification_codes_actual: number;
|
||||
note: string;
|
||||
}
|
||||
|
||||
// ==================== Indices ====================
|
||||
|
||||
export interface TaxonomyIndices {
|
||||
all_domains: string[];
|
||||
all_categories: string[];
|
||||
all_subcodes: string[];
|
||||
all_causal_codes: string[];
|
||||
subcodes_by_domain: Record<string, number>;
|
||||
}
|
||||
|
||||
// ==================== Main Taxonomy ====================
|
||||
|
||||
export interface URTTaxonomy {
|
||||
version: string;
|
||||
status: string;
|
||||
release_date: string;
|
||||
statistics: TaxonomyStatistics;
|
||||
domains: Record<string, Domain>;
|
||||
causal_codes: Record<string, CausalLayer>;
|
||||
metadata_dimensions: Record<string, MetadataDimension>;
|
||||
profiles: Record<string, Profile>;
|
||||
indices: TaxonomyIndices;
|
||||
}
|
||||
|
||||
// ==================== UI State Types ====================
|
||||
|
||||
export type TaxonomyTab = 'codes' | 'causal' | 'metadata' | 'profiles';
|
||||
|
||||
export interface TreeNodeState {
|
||||
expanded: boolean;
|
||||
}
|
||||
|
||||
export interface SelectedSubcode {
|
||||
code: string;
|
||||
domainKey: string;
|
||||
domainName: string;
|
||||
categoryKey: string;
|
||||
categoryName: string;
|
||||
subcode: Subcode;
|
||||
}
|
||||
|
||||
// ==================== Domain Colors ====================
|
||||
|
||||
export const DOMAIN_COLORS: Record<string, string> = {
|
||||
O: '#f97316', // Offering - orange
|
||||
P: '#3b82f6', // People - blue
|
||||
J: '#8b5cf6', // Journey - purple
|
||||
E: '#06b6d4', // Environment - cyan
|
||||
A: '#10b981', // Access - green
|
||||
V: '#ec4899', // Value - pink
|
||||
R: '#f59e0b', // Relationship - amber
|
||||
};
|
||||
|
||||
export const DOMAIN_BG_COLORS: Record<string, string> = {
|
||||
O: 'bg-orange-500/10',
|
||||
P: 'bg-blue-500/10',
|
||||
J: 'bg-purple-500/10',
|
||||
E: 'bg-cyan-500/10',
|
||||
A: 'bg-emerald-500/10',
|
||||
V: 'bg-pink-500/10',
|
||||
R: 'bg-amber-500/10',
|
||||
};
|
||||
|
||||
export const DOMAIN_BORDER_COLORS: Record<string, string> = {
|
||||
O: 'border-orange-500/30',
|
||||
P: 'border-blue-500/30',
|
||||
J: 'border-purple-500/30',
|
||||
E: 'border-cyan-500/30',
|
||||
A: 'border-emerald-500/30',
|
||||
V: 'border-pink-500/30',
|
||||
R: 'border-amber-500/30',
|
||||
};
|
||||
|
||||
export const DOMAIN_TEXT_COLORS: Record<string, string> = {
|
||||
O: 'text-orange-400',
|
||||
P: 'text-blue-400',
|
||||
J: 'text-purple-400',
|
||||
E: 'text-cyan-400',
|
||||
A: 'text-emerald-400',
|
||||
V: 'text-pink-400',
|
||||
R: 'text-amber-400',
|
||||
};
|
||||
Reference in New Issue
Block a user