Job analysis in flight

This commit is contained in:
James Ketr 2025-06-03 17:00:18 -07:00
parent 05c53653ed
commit cb97cabfc3
4 changed files with 575 additions and 100 deletions

View File

@ -39,6 +39,8 @@ import { useNavigate } from 'react-router-dom';
import { BackstoryPageProps } from 'components/BackstoryTab';
import { useAuth } from 'hooks/AuthContext';
import { useSelectedCandidate } from 'hooks/GlobalContext';
import { CandidateInfo } from 'components/CandidateInfo';
import { ComingSoon } from 'components/ui/ComingSoon';
// Main component
const JobAnalysisPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps) => {
@ -93,32 +95,14 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps
}, [selectedCandidate, activeStep]);
// Steps in our process
const steps = selectedCandidate === null ? [
{ index: 0, label: 'Select Candidate', icon: <PersonIcon /> },
const steps = [
{ index: 1, label: 'Job Description', icon: <WorkIcon /> },
{ index: 2, label: 'View Analysis', icon: <AssessmentIcon /> }
] : [
{ index: 1, label: 'Job Description', icon: <WorkIcon /> },
{ index: 2, label: 'View Analysis', icon: <AssessmentIcon /> }
{ index: 2, label: 'AI Analysis', icon: <WorkIcon /> },
{ index: 3, label: 'Generated Resume', icon: <AssessmentIcon /> }
];
// Mock handlers for our analysis APIs
const fetchRequirements = async (): Promise<string[]> => {
// Simulates extracting requirements from the job description
await new Promise(resolve => setTimeout(resolve, 2000));
// This would normally parse the job description to extract requirements
const mockRequirements = [
"5+ years of React development experience",
"Strong TypeScript skills",
"Experience with RESTful APIs",
"Knowledge of state management solutions (Redux, Context API)",
"Experience with CI/CD pipelines",
"Cloud platform experience (AWS, Azure, GCP)"
];
return mockRequirements;
};
if (!selectedCandidate) {
steps.unshift({ index: 0, label: 'Select Candidate', icon: <PersonIcon /> })
}
const fetchMatchForRequirement = async (requirement: string): Promise<any> => {
// Create different mock responses based on the requirement
@ -245,9 +229,11 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps
return;
}
if (activeStep === 1 && (/*(extraInfo && !jobTitle) || */!jobDescription)) {
setError('Please provide both job title and description before continuing.');
return;
if (activeStep === 1) {
if ((/*(extraInfo && !jobTitle) || */!jobDescription)) {
setError('Please provide job description before continuing.');
return;
}
}
if (activeStep === 2) {
@ -433,15 +419,19 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps
<Box sx={{ mt: 3 }}>
{selectedCandidate && (
<JobMatchAnalysis
jobTitle={jobTitle}
candidateName={selectedCandidate.fullName}
fetchRequirements={fetchRequirements}
fetchMatchForRequirement={fetchMatchForRequirement}
job={{ title: jobTitle, description: jobDescription }}
candidate={selectedCandidate}
/>
)}
</Box>
);
const renderResume = () => (
<Box sx={{ mt: 3 }}>
{selectedCandidate && <ComingSoon>Resume Builder</ComingSoon>}
</Box>
);
// If no user is logged in, show message
if (!user) {
return (
@ -464,6 +454,7 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps
<Typography variant="h4" component="h1" gutterBottom>
Candidate Analysis
</Typography>
{selectedCandidate && <CandidateInfo variant="small" candidate={selectedCandidate} />}
<Typography variant="subtitle1" color="text.secondary" gutterBottom>
Match candidates to job requirements with AI-powered analysis
</Typography>
@ -496,6 +487,7 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps
{activeStep === 0 && renderCandidateSelection()}
{activeStep === 1 && renderJobDescription()}
{activeStep === 2 && renderAnalysis()}
{activeStep === 3 && renderResume()}
<Box sx={{ display: 'flex', flexDirection: 'row', pt: 2 }}>
<Button

View File

@ -1,6 +1,6 @@
// Generated TypeScript types from Pydantic models
// Source: src/backend/models.py
// Generated on: 2025-06-03T18:51:32.304683
// Generated on: 2025-06-03T23:59:28.355326
// DO NOT EDIT MANUALLY - This file is auto-generated
// ============================
@ -13,13 +13,13 @@ export type ActivityType = "login" | "search" | "view_job" | "apply_job" | "mess
export type ApplicationStatus = "applied" | "reviewing" | "interview" | "offer" | "rejected" | "accepted" | "withdrawn";
export type ChatContextType = "job_search" | "candidate_chat" | "interview_prep" | "resume_review" | "general" | "generate_persona" | "generate_profile" | "generate_image" | "rag_search";
export type ChatContextType = "job_search" | "job_requirements" | "candidate_chat" | "interview_prep" | "resume_review" | "general" | "generate_persona" | "generate_profile" | "generate_image" | "rag_search";
export type ChatMessageType = "error" | "generating" | "info" | "preparing" | "processing" | "heartbeat" | "response" | "searching" | "rag_result" | "system" | "thinking" | "tooling" | "user";
export type ChatSenderType = "user" | "assistant" | "system";
export type ChatStatusType = "initializing" | "streaming" | "done" | "error";
export type ChatStatusType = "initializing" | "streaming" | "status" | "done" | "error";
export type ColorBlindMode = "protanopia" | "deuteranopia" | "tritanopia" | "none";
@ -272,7 +272,7 @@ export interface Certification {
}
export interface ChatContext {
type: "job_search" | "candidate_chat" | "interview_prep" | "resume_review" | "general" | "generate_persona" | "generate_profile" | "generate_image" | "rag_search";
type: "job_search" | "job_requirements" | "candidate_chat" | "interview_prep" | "resume_review" | "general" | "generate_persona" | "generate_profile" | "generate_image" | "rag_search";
relatedEntityId?: string;
relatedEntityType?: "job" | "candidate" | "employer";
additionalContext?: Record<string, any>;
@ -282,10 +282,10 @@ export interface ChatMessage {
id?: string;
sessionId: string;
senderId?: string;
status: "initializing" | "streaming" | "done" | "error";
status: "initializing" | "streaming" | "status" | "done" | "error";
type: "error" | "generating" | "info" | "preparing" | "processing" | "heartbeat" | "response" | "searching" | "rag_result" | "system" | "thinking" | "tooling" | "user";
sender: "user" | "assistant" | "system";
timestamp: Date;
timestamp?: Date;
tunables?: Tunables;
content: string;
metadata?: ChatMessageMetaData;
@ -295,16 +295,16 @@ export interface ChatMessageBase {
id?: string;
sessionId: string;
senderId?: string;
status: "initializing" | "streaming" | "done" | "error";
status: "initializing" | "streaming" | "status" | "done" | "error";
type: "error" | "generating" | "info" | "preparing" | "processing" | "heartbeat" | "response" | "searching" | "rag_result" | "system" | "thinking" | "tooling" | "user";
sender: "user" | "assistant" | "system";
timestamp: Date;
timestamp?: Date;
tunables?: Tunables;
content: string;
}
export interface ChatMessageMetaData {
model: "qwen2.5";
model: "qwen2.5" | "flux-schnell";
temperature: number;
maxTokens: number;
topP: number;
@ -326,10 +326,10 @@ export interface ChatMessageRagSearch {
id?: string;
sessionId: string;
senderId?: string;
status: "done";
type: "rag_result";
sender: "user";
timestamp: Date;
status: "initializing" | "streaming" | "status" | "done" | "error";
type: "error" | "generating" | "info" | "preparing" | "processing" | "heartbeat" | "response" | "searching" | "rag_result" | "system" | "thinking" | "tooling" | "user";
sender: "user" | "assistant" | "system";
timestamp?: Date;
tunables?: Tunables;
content: string;
dimensions: number;
@ -339,10 +339,10 @@ export interface ChatMessageUser {
id?: string;
sessionId: string;
senderId?: string;
status: "done";
type: "user";
sender: "user";
timestamp: Date;
status: "initializing" | "streaming" | "status" | "done" | "error";
type: "error" | "generating" | "info" | "preparing" | "processing" | "heartbeat" | "response" | "searching" | "rag_result" | "system" | "thinking" | "tooling" | "user";
sender: "user" | "assistant" | "system";
timestamp?: Date;
tunables?: Tunables;
content: string;
}
@ -857,6 +857,233 @@ export interface WorkExperience {
achievements?: Array<string>;
}
// ============================
// Default Objects
// ============================
// These objects contain the default values from your Pydantic models
// Use them to initialize objects with sensible defaults:
// const message: ChatMessage = { ...DefaultChatMessage, sessionId: '123', content: 'Hello' };
/**
* Default values for BaseUser
* Fields with defaults: isAdmin
*/
export const DefaultBaseUser: Partial<BaseUser> = {
isAdmin: False
};
/**
* Default values for BaseUserWithType
* Fields with defaults: isAdmin
*/
export const DefaultBaseUserWithType: Partial<BaseUserWithType> = {
isAdmin: False
};
/**
* Default values for Candidate
* Fields with defaults: isAdmin, userType, ragContentSize
*/
export const DefaultCandidate: Partial<Candidate> = {
isAdmin: False,
userType: "candidate",
ragContentSize: 0
};
/**
* Default values for CandidateAI
* Fields with defaults: isAdmin, userType, ragContentSize, isAI
*/
export const DefaultCandidateAI: Partial<CandidateAI> = {
isAdmin: False,
userType: "candidate",
ragContentSize: 0,
isAI: True
};
/**
* Default values for ChatContext
* Fields with defaults: additionalContext
*/
export const DefaultChatContext: Partial<ChatContext> = {
additionalContext: {}
};
/**
* Default values for ChatMessage
* Fields with defaults: status, type, sender, content
*/
export const DefaultChatMessage: Partial<ChatMessage> = {
status: "initializing",
type: "preparing",
sender: "system",
content: ""
};
/**
* Default values for ChatMessageBase
* Fields with defaults: status, type, sender, content
*/
export const DefaultChatMessageBase: Partial<ChatMessageBase> = {
status: "initializing",
type: "preparing",
sender: "system",
content: ""
};
/**
* Default values for ChatMessageMetaData
* Fields with defaults: model, temperature, maxTokens, topP, evalCount, evalDuration, promptEvalCount, promptEvalDuration
*/
export const DefaultChatMessageMetaData: Partial<ChatMessageMetaData> = {
model: "qwen2.5",
temperature: 0.7,
maxTokens: 8092,
topP: 1,
evalCount: 0,
evalDuration: 0,
promptEvalCount: 0,
promptEvalDuration: 0
};
/**
* Default values for ChatMessageRagSearch
* Fields with defaults: status, type, sender, content, dimensions
*/
export const DefaultChatMessageRagSearch: Partial<ChatMessageRagSearch> = {
status: "done",
type: "rag_result",
sender: "user",
content: "",
dimensions: 3
};
/**
* Default values for ChatMessageUser
* Fields with defaults: status, type, sender, content
*/
export const DefaultChatMessageUser: Partial<ChatMessageUser> = {
status: "done",
type: "user",
sender: "user",
content: ""
};
/**
* Default values for ChatOptions
* Fields with defaults: seed, temperature
*/
export const DefaultChatOptions: Partial<ChatOptions> = {
seed: 8911,
temperature: 0.7
};
/**
* Default values for ChatSession
* Fields with defaults: isArchived
*/
export const DefaultChatSession: Partial<ChatSession> = {
isArchived: False
};
/**
* Default values for ChromaDBGetResponse
* Fields with defaults: ids, embeddings, documents, metadatas, distances, name, size, dimensions, query
*/
export const DefaultChromaDBGetResponse: Partial<ChromaDBGetResponse> = {
ids: [],
embeddings: [],
documents: [],
metadatas: [],
distances: [],
name: "",
size: 0,
dimensions: 3,
query: ""
};
/**
* Default values for Document
* Fields with defaults: includeInRAG, ragChunks
*/
export const DefaultDocument: Partial<Document> = {
includeInRAG: True,
ragChunks: 0
};
/**
* Default values for Employer
* Fields with defaults: isAdmin, userType
*/
export const DefaultEmployer: Partial<Employer> = {
isAdmin: False,
userType: "employer"
};
/**
* Default values for Job
* Fields with defaults: views, applicationCount
*/
export const DefaultJob: Partial<Job> = {
views: 0,
applicationCount: 0
};
/**
* Default values for LLMMessage
* Fields with defaults: role, content, toolCalls
*/
export const DefaultLLMMessage: Partial<LLMMessage> = {
role: "",
content: "",
toolCalls: {}
};
/**
* Default values for MFAVerifyRequest
* Fields with defaults: rememberDevice
*/
export const DefaultMFAVerifyRequest: Partial<MFAVerifyRequest> = {
rememberDevice: False
};
/**
* Default values for PaginatedRequest
* Fields with defaults: page, limit
*/
export const DefaultPaginatedRequest: Partial<PaginatedRequest> = {
page: 1,
limit: 20
};
/**
* Default values for RagEntry
* Fields with defaults: description, enabled
*/
export const DefaultRagEntry: Partial<RagEntry> = {
description: "",
enabled: True
};
/**
* Default values for SearchQuery
* Fields with defaults: page, limit
*/
export const DefaultSearchQuery: Partial<SearchQuery> = {
page: 1,
limit: 20
};
/**
* Default values for Tunables
* Fields with defaults: enableRAG, enableTools, enableContext
*/
export const DefaultTunables: Partial<Tunables> = {
enableRAG: True,
enableTools: True,
enableContext: True
};
// ============================
// Date Conversion Functions
// ============================
@ -1017,7 +1244,7 @@ export function convertChatMessageFromApi(data: any): ChatMessage {
return {
...data,
// Convert timestamp from ISO string to Date
timestamp: new Date(data.timestamp),
timestamp: data.timestamp ? new Date(data.timestamp) : undefined,
};
}
/**
@ -1030,7 +1257,7 @@ export function convertChatMessageBaseFromApi(data: any): ChatMessageBase {
return {
...data,
// Convert timestamp from ISO string to Date
timestamp: new Date(data.timestamp),
timestamp: data.timestamp ? new Date(data.timestamp) : undefined,
};
}
/**
@ -1043,7 +1270,7 @@ export function convertChatMessageRagSearchFromApi(data: any): ChatMessageRagSea
return {
...data,
// Convert timestamp from ISO string to Date
timestamp: new Date(data.timestamp),
timestamp: data.timestamp ? new Date(data.timestamp) : undefined,
};
}
/**
@ -1056,7 +1283,7 @@ export function convertChatMessageUserFromApi(data: any): ChatMessageUser {
return {
...data,
// Convert timestamp from ISO string to Date
timestamp: new Date(data.timestamp),
timestamp: data.timestamp ? new Date(data.timestamp) : undefined,
};
}
/**

View File

@ -138,10 +138,14 @@ def is_date_type(python_type: Any) -> bool:
return False
def get_default_enum_value(field_info: Any, debug: bool = False) -> Optional[Any]:
"""Extract the specific enum value from a field's default, if it exists"""
def get_field_default_value(field_info: Any, debug: bool = False) -> tuple[bool, Any]:
"""Extract the default value from a field, if it exists
Returns:
tuple: (has_default, default_value)
"""
if not hasattr(field_info, 'default'):
return None
return False, None
default_val = field_info.default
@ -152,7 +156,7 @@ def get_default_enum_value(field_info: Any, debug: bool = False) -> Optional[Any
if default_val is ... or default_val is None:
if debug:
print(f" └─ Default is undefined marker")
return None
return False, None
# Check for Pydantic's internal "PydanticUndefined" or similar markers
default_str = str(default_val)
@ -173,17 +177,72 @@ def get_default_enum_value(field_info: Any, debug: bool = False) -> Optional[Any
if is_undefined_marker:
if debug:
print(f" └─ Default is undefined marker pattern")
return None
# Check if it's an enum instance
if isinstance(default_val, Enum):
if debug:
print(f" └─ Default is enum instance: {default_val.value}")
return default_val
return False, None
# We have a real default value
if debug:
print(f" └─ Default is not an enum instance")
return None
print(f" └─ Has real default value: {repr(default_val)}")
return True, default_val
def convert_default_to_typescript(default_val: Any, debug: bool = False) -> str:
"""Convert a Python default value to TypeScript literal"""
if debug:
print(f" 🔄 Converting default: {repr(default_val)} (type: {type(default_val)})")
# Handle None
if default_val is None:
return "undefined"
# Handle Enum instances
if isinstance(default_val, Enum):
return f'"{default_val.value}"'
# Handle basic types
if isinstance(default_val, str):
# Escape quotes and special characters
escaped = default_val.replace('\\', '\\\\').replace('"', '\\"')
return f'"{escaped}"'
elif isinstance(default_val, (int, float)):
return str(default_val)
elif isinstance(default_val, bool):
return "true" if default_val else "false"
elif isinstance(default_val, list):
if not default_val: # Empty list
return "[]"
# For non-empty lists, convert each item
items = [convert_default_to_typescript(item, debug) for item in default_val]
return f"[{', '.join(items)}]"
elif isinstance(default_val, dict):
if not default_val: # Empty dict
return "{}"
# For non-empty dicts, convert each key-value pair
items = []
for key, value in default_val.items():
key_str = f'"{key}"' if isinstance(key, str) else str(key)
value_str = convert_default_to_typescript(value, debug)
items.append(f"{key_str}: {value_str}")
return f"{{{', '.join(items)}}}"
elif isinstance(default_val, datetime):
# Convert datetime to ISO string, then wrap in new Date()
iso_string = default_val.isoformat()
return f'new Date("{iso_string}")'
# For other types, try to convert to string
if debug:
print(f" ⚠️ Unknown default type, converting to string: {type(default_val)}")
# Try to convert to a reasonable TypeScript representation
try:
if hasattr(default_val, '__dict__'):
# It's an object, try to serialize its properties
return "{}" # Fallback to empty object for complex types
else:
# Try string conversion
str_val = str(default_val)
escaped = str_val.replace('\\', '\\\\').replace('"', '\\"')
return f'"{escaped}"'
except:
return "undefined"
def python_type_to_typescript(python_type: Any, field_info: Any = None, debug: bool = False) -> str:
"""Convert a Python type to TypeScript type string, considering field defaults"""
@ -198,13 +257,9 @@ def python_type_to_typescript(python_type: Any, field_info: Any = None, debug: b
if debug and original_type != python_type:
print(f" 🔄 Unwrapped: {original_type} -> {python_type}")
# Check if this field has a specific enum default value
if field_info:
default_enum = get_default_enum_value(field_info, debug)
if default_enum is not None:
if debug:
print(f" 🎯 Field has specific enum default: {default_enum.value}")
return f'"{default_enum.value}"'
# REMOVED: The problematic enum default checking that returns only the default value
# This was causing the issue where enum fields would only show the default value
# instead of all possible enum values
# Handle None/null
if python_type is type(None):
@ -268,9 +323,12 @@ def python_type_to_typescript(python_type: Any, field_info: Any = None, debug: b
literal_values.append(str(arg))
return " | ".join(literal_values)
# Handle Enum types
# Handle Enum types - THIS IS THE CORRECT BEHAVIOR
# Return all possible enum values, not just the default
if isinstance(python_type, type) and issubclass(python_type, Enum):
enum_values = [f'"{v.value}"' for v in python_type]
if debug:
print(f" 🎯 Enum type detected: {python_type.__name__} with values: {enum_values}")
return " | ".join(enum_values)
# Handle individual enum instances
@ -375,18 +433,12 @@ def is_field_optional(field_info: Any, field_type: Any, debug: bool = False) ->
print(f" └─ RESULT: Required (default is undefined marker)")
return False
# Special case: if field has a specific default value (like enum), it's required
# because it will always have a value, just not optional for the consumer
if isinstance(default_val, Enum):
if debug:
print(f" └─ RESULT: Required (has specific enum default: {default_val.value})")
return False
# FIXED: Fields with actual default values (like [], "", 0) should be REQUIRED
# FIXED: Fields with actual default values (including enums) should be REQUIRED
# because they will always have a value (either provided or the default)
# This applies to enum fields with defaults as well
if debug:
print(f" └─ RESULT: Required (has actual default value - field will always have a value)")
return False # Changed from True to False
return False
else:
if debug:
print(f" └─ No default attribute found")
@ -420,6 +472,7 @@ def process_pydantic_model(model_class, debug: bool = False) -> Dict[str, Any]:
interface_name = model_class.__name__
properties = []
date_fields = [] # Track date fields for conversion functions
default_fields = [] # Track fields with default values for default object generation
if debug:
print(f" 🔍 Processing model: {interface_name}")
@ -445,6 +498,17 @@ def process_pydantic_model(model_class, debug: bool = False) -> Dict[str, Any]:
if debug:
print(f" Raw type: {field_type}")
# Check for default values
has_default, default_value = get_field_default_value(field_info, debug)
if has_default:
ts_default = convert_default_to_typescript(default_value, debug)
default_fields.append({
'name': ts_name,
'value': ts_default
})
if debug:
print(f" 🎯 Default value: {repr(default_value)} -> {ts_default}")
# Check if this is a date field
is_date = is_date_type(field_type)
if debug:
@ -461,7 +525,7 @@ def process_pydantic_model(model_class, debug: bool = False) -> Dict[str, Any]:
elif debug and ('date' in str(field_type).lower() or 'time' in str(field_type).lower()):
print(f" ⚠️ Field {ts_name} contains 'date'/'time' but not detected as date type: {field_type}")
# Pass field_info to the type converter for default enum handling
# Pass field_info to the type converter (but now it won't override enum types)
ts_type = python_type_to_typescript(field_type, field_info, debug)
# Check if optional
@ -497,6 +561,17 @@ def process_pydantic_model(model_class, debug: bool = False) -> Dict[str, Any]:
if debug:
print(f" Raw type: {field_type}")
# Check for default values
has_default, default_value = get_field_default_value(field_info, debug)
if has_default:
ts_default = convert_default_to_typescript(default_value, debug)
default_fields.append({
'name': ts_name,
'value': ts_default
})
if debug:
print(f" 🎯 Default value: {repr(default_value)} -> {ts_default}")
# Check if this is a date field
is_date = is_date_type(field_type)
if debug:
@ -513,7 +588,7 @@ def process_pydantic_model(model_class, debug: bool = False) -> Dict[str, Any]:
elif debug and ('date' in str(field_type).lower() or 'time' in str(field_type).lower()):
print(f" ⚠️ Field {ts_name} contains 'date'/'time' but not detected as date type: {field_type}")
# Pass field_info to the type converter for default enum handling
# Pass field_info to the type converter (but now it won't override enum types)
ts_type = python_type_to_typescript(field_type, field_info, debug)
# For Pydantic v1, check required and default
@ -534,7 +609,8 @@ def process_pydantic_model(model_class, debug: bool = False) -> Dict[str, Any]:
return {
'name': interface_name,
'properties': properties,
'date_fields': date_fields
'date_fields': date_fields,
'default_fields': default_fields
}
def process_enum(enum_class) -> Dict[str, Any]:
@ -548,6 +624,159 @@ def process_enum(enum_class) -> Dict[str, Any]:
'values': " | ".join(values)
}
def generate_default_objects(interfaces: List[Dict[str, Any]]) -> str:
"""Generate TypeScript default objects for models with default values"""
default_objects = []
for interface in interfaces:
interface_name = interface['name']
default_fields = interface.get('default_fields', [])
if not default_fields:
continue # Skip interfaces without default values
object_name = f"Default{interface_name}"
# Generate default object
obj_lines = [
f"/**",
f" * Default values for {interface_name}",
f" * Fields with defaults: {', '.join([f['name'] for f in default_fields])}",
f" */",
f"export const {object_name}: Partial<{interface_name}> = {{"
]
# Add default field values
for i, default_field in enumerate(default_fields):
field_name = default_field['name']
field_value = default_field['value']
# Add comma for all but the last field
comma = "," if i < len(default_fields) - 1 else ""
obj_lines.append(f" {field_name}: {field_value}{comma}")
obj_lines.append("};")
obj_lines.append("") # Empty line after each object
default_objects.append('\n'.join(obj_lines))
if not default_objects:
return ""
# Generate the default objects section
result = [
"// ============================",
"// Default Objects",
"// ============================",
"",
"// These objects contain the default values from your Pydantic models",
"// Use them to initialize objects with sensible defaults:",
"// const message: ChatMessage = { ...DefaultChatMessage, sessionId: '123', content: 'Hello' };",
"",
]
result.extend(default_objects)
return '\n'.join(result)
"""Generate TypeScript conversion functions for models with date fields"""
conversion_functions = []
for interface in interfaces:
interface_name = interface['name']
date_fields = interface.get('date_fields', [])
if not date_fields:
continue # Skip interfaces without date fields
function_name = f"convert{interface_name}FromApi"
# Generate function
func_lines = [
f"/**",
f" * Convert {interface_name} from API response, parsing date fields",
f" * Date fields: {', '.join([f['name'] for f in date_fields])}",
f" */",
f"export function {function_name}(data: any): {interface_name} {{",
f" if (!data) return data;",
f" ",
f" return {{",
f" ...data,"
]
# Add date field conversions with validation
for date_field in date_fields:
field_name = date_field['name']
is_optional = date_field['optional']
# Add a comment for clarity
func_lines.append(f" // Convert {field_name} from ISO string to Date")
if is_optional:
func_lines.append(f" {field_name}: data.{field_name} ? new Date(data.{field_name}) : undefined,")
else:
func_lines.append(f" {field_name}: new Date(data.{field_name}),")
func_lines.extend([
f" }};",
f"}}"
])
conversion_functions.append('\n'.join(func_lines))
if not conversion_functions:
return ""
# Generate the conversion functions section
result = [
"// ============================",
"// Date Conversion Functions",
"// ============================",
"",
"// These functions convert API responses to properly typed objects",
"// with Date objects instead of ISO date strings",
"",
]
result.extend(conversion_functions)
result.append("")
# Generate a generic converter function
models_with_dates = [interface['name'] for interface in interfaces if interface.get('date_fields')]
if models_with_dates:
result.extend([
"/**",
" * Generic converter that automatically selects the right conversion function",
" * based on the model type",
" */",
"export function convertFromApi<T>(data: any, modelType: string): T {",
" if (!data) return data;",
" ",
" switch (modelType) {"
])
for model_name in models_with_dates:
result.append(f" case '{model_name}':")
result.append(f" return convert{model_name}FromApi(data) as T;")
result.extend([
" default:",
" return data as T;",
" }",
"}",
"",
"/**",
" * Convert array of items using the appropriate converter",
" */",
"export function convertArrayFromApi<T>(data: any[], modelType: string): T[] {",
" if (!data || !Array.isArray(data)) return data;",
" return data.map(item => convertFromApi<T>(item, modelType));",
"}",
""
])
return '\n'.join(result)
def generate_conversion_functions(interfaces: List[Dict[str, Any]]) -> str:
"""Generate TypeScript conversion functions for models with date fields"""
conversion_functions = []
@ -706,8 +935,10 @@ def generate_typescript_interfaces(source_file: str, debug: bool = False):
continue
total_date_fields = sum(len(interface.get('date_fields', [])) for interface in interfaces)
total_default_fields = sum(len(interface.get('default_fields', [])) for interface in interfaces)
print(f"\n📊 Found {len(interfaces)} interfaces and {len(enums)} enums")
print(f"🗓️ Found {total_date_fields} date fields across all models")
print(f"🎯 Found {total_default_fields} fields with default values across all models")
# Generate TypeScript content
ts_content = f"""// Generated TypeScript types from Pydantic models
@ -741,6 +972,11 @@ def generate_typescript_interfaces(source_file: str, debug: bool = False):
ts_content += "}\n\n"
# Add default objects
default_objects = generate_default_objects(interfaces)
if default_objects:
ts_content += default_objects
# Add conversion functions
conversion_functions = generate_conversion_functions(interfaces)
if conversion_functions:
@ -780,7 +1016,7 @@ def compile_typescript(ts_file: str) -> bool:
def main():
"""Main function with command line argument parsing"""
parser = argparse.ArgumentParser(
description='Generate TypeScript types from Pydantic models with date conversion functions and proper enum default handling',
description='Generate TypeScript types from Pydantic models with date conversion functions, default objects, and proper enum handling',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
@ -795,8 +1031,12 @@ Generated conversion functions can be used like:
const candidate = convertCandidateFromApi(apiResponse);
const jobs = convertArrayFromApi<Job>(apiResponse, 'Job');
Enum defaults are now properly handled:
status: ChatStatusType = ChatStatusType.DONE -> status: "done"
Generated default objects can be used like:
const message: ChatMessage = { ...DefaultChatMessage, sessionId: '123', content: 'Hello' };
const overrideMessage: ChatMessage = { ...DefaultChatMessage, status: 'error' };
Enum fields now properly support all enum values:
status: ChatStatusType = ChatStatusType.DONE -> status: "pending" | "processing" | "done" | "error"
"""
)
@ -833,12 +1073,12 @@ Enum defaults are now properly handled:
parser.add_argument(
'--version', '-v',
action='version',
version='TypeScript Generator 3.1 (with Enum Default Handling)'
version='TypeScript Generator 3.3 (with Default Objects and Fixed Enum Handling)'
)
args = parser.parse_args()
print("🚀 Enhanced TypeScript Type Generator with Enum Default Handling")
print("🚀 Enhanced TypeScript Type Generator with Default Objects and Fixed Enum Handling")
print("=" * 60)
print(f"📁 Source file: {args.source}")
print(f"📁 Output file: {args.output}")
@ -883,27 +1123,37 @@ Enum defaults are now properly handled:
# Count conversion functions and provide detailed feedback
conversion_count = ts_content.count('export function convert') - ts_content.count('convertFromApi') - ts_content.count('convertArrayFromApi')
enum_specific_count = ts_content.count(': "') - ts_content.count('export type')
default_objects_count = ts_content.count('export const Default')
enum_union_count = ts_content.count(' | ')
if conversion_count > 0:
print(f"🗓️ Generated {conversion_count} date conversion functions")
if enum_specific_count > 0:
print(f"🎯 Generated {enum_specific_count} specific enum default types")
if default_objects_count > 0:
print(f"🎯 Generated {default_objects_count} default objects")
if enum_union_count > 0:
print(f"🔗 Generated {enum_union_count} union types (including proper enum types)")
if args.debug:
# Show which models have date conversion
models_with_dates = []
models_with_defaults = []
for line in ts_content.split('\n'):
if line.startswith('export function convert') and 'FromApi' in line and 'convertFromApi' not in line:
model_name = line.split('convert')[1].split('FromApi')[0]
models_with_dates.append(model_name)
elif line.startswith('export const Default'):
model_name = line.split('export const Default')[1].split(':')[0]
models_with_defaults.append(model_name)
if models_with_dates:
print(f" Models with date conversion: {', '.join(models_with_dates)}")
if models_with_defaults:
print(f" Models with default objects: {', '.join(models_with_defaults)}")
# Provide troubleshooting info if debug mode
if args.debug:
print(f"\n🐛 Debug mode was enabled. If you see incorrect type conversions:")
print(f" 1. Check the debug output above for '🎯 Field has specific enum default' lines")
print(f" 1. Look for '🎯 Enum type detected' lines to verify enum handling")
print(f" 2. Look for '📅 Date type check' lines for date handling")
print(f" 3. Look for '⚠️' warnings about fallback types")
print(f" 4. Verify your Pydantic model field types and defaults are correct")
@ -924,19 +1174,24 @@ Enum defaults are now properly handled:
print(f"✅ File size: {file_size} characters")
if conversion_count > 0:
print(f"✅ Date conversion functions: {conversion_count}")
if enum_specific_count > 0:
print(f"✅ Specific enum default types: {enum_specific_count}")
if default_objects_count > 0:
print(f"✅ Default objects: {default_objects_count}")
if enum_union_count > 0:
print(f"✅ Union types (proper enum support): {enum_union_count}")
if not args.skip_test:
print("✅ Model validation passed")
if not args.skip_compile:
print("✅ TypeScript syntax validated")
print(f"\n💡 Usage in your TypeScript project:")
print(f" import {{ Candidate, Employer, Job, convertCandidateFromApi }} from './{Path(args.output).stem}';")
print(f" import {{ ChatMessage, ChatStatusType, DefaultChatMessage, convertChatMessageFromApi }} from './{Path(args.output).stem}';")
print(f" const message: ChatMessage = {{ ...DefaultChatMessage, sessionId: '123', content: 'Hello' }};")
if conversion_count > 0:
print(f" const candidate = convertCandidateFromApi(apiResponse);")
print(f" const jobs = convertArrayFromApi<Job>(apiResponse, 'Job');")
print(f" const message = convertChatMessageFromApi(apiResponse);")
print(f" const messages = convertArrayFromApi<ChatMessage>(apiResponse, 'ChatMessage');")
if default_objects_count > 0:
print(f" const overrideMessage: ChatMessage = {{ ...DefaultChatMessage, status: 'error' }};")
return True
except KeyboardInterrupt:

View File

@ -97,6 +97,7 @@ class ChatStatusType(str, Enum):
class ChatContextType(str, Enum):
JOB_SEARCH = "job_search"
JOB_REQUIREMENTS = "job_requirements"
CANDIDATE_CHAT = "candidate_chat"
INTERVIEW_PREP = "interview_prep"
RESUME_REVIEW = "resume_review"