Job analysis in flight
This commit is contained in:
parent
05c53653ed
commit
cb97cabfc3
@ -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
|
||||
|
@ -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,
|
||||
};
|
||||
}
|
||||
/**
|
||||
|
@ -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:
|
||||
|
@ -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"
|
||||
|
Loading…
x
Reference in New Issue
Block a user