1185 lines
63 KiB
Python
1185 lines
63 KiB
Python
"""
|
|
Formal Letters Module
|
|
|
|
This module provides a Streamlit interface for generating various types of formal letters
|
|
using AI assistance. It collects user inputs specific to the chosen formal letter subtype,
|
|
formats the data, generates a prompt for the AI, calls the AI for content generation,
|
|
and displays the formatted letter preview and analysis.
|
|
"""
|
|
|
|
import streamlit as st
|
|
import datetime
|
|
from typing import Dict, Any, List
|
|
|
|
# Assuming these modules and functions exist and are correctly imported in a real application.
|
|
# Placeholder functions are included below for demonstration purposes if actual imports are not available.
|
|
# from ..utils.letter_formatter import format_letter, get_letter_preview_html
|
|
# from ..utils.letter_analyzer import analyze_letter_tone, check_formality, get_readability_metrics, suggest_improvements
|
|
# from ..utils.letter_templates import get_template_by_type
|
|
# from ....gpt_providers.text_generation.main_text_generation import llm_text_gen
|
|
|
|
# --- Placeholder Functions (Replace with actual imports in a real app) ---
|
|
# These placeholders mimic the expected behavior of the imported functions
|
|
# to allow the rest of the code structure to be reviewed and run without dependencies.
|
|
|
|
def format_letter(content: str, metadata: Dict[str, Any], letter_type: str = "personal") -> str:
|
|
"""Placeholder: Returns the content as is."""
|
|
return content
|
|
|
|
def get_letter_preview_html(content: str, metadata: Dict[str, Any], letter_type: str = "personal") -> str:
|
|
"""Placeholder: Generates a basic HTML preview for formal letters."""
|
|
# Basic HTML structure with inline styles for preview
|
|
formatted_paragraphs = "".join(f"<p style='margin-bottom: 1em;'>{p.strip()}</p>" for p in content.split("\n\n") if p.strip())
|
|
return f"""
|
|
<div style="max-width: 800px; margin: 20px auto; padding: 30px; border: 1px solid #d0d0d0; border-radius: 8px; background-color: #ffffff; font-family: 'Arial', sans-serif; line-height: 1.6; color: #333;">
|
|
<div style="text-align: right; margin-bottom: 20px;">{metadata.get('date', 'Date')}</div>
|
|
<div style="margin-bottom: 20px; font-weight: bold;">Subject: {metadata.get('subject', 'No Subject')}</div>
|
|
<div style="margin-bottom: 20px;">{metadata.get('salutation', 'Dear Recipient,')}</div>
|
|
<div style="margin-bottom: 20px;">{formatted_paragraphs if formatted_paragraphs else "<p>Letter content goes here...</p>"}</div>
|
|
<div style="margin-top: 40px;">{metadata.get('complimentary_close', 'Sincerely,')}</div>
|
|
<div>{metadata.get('sender_name', 'Sender Name')}</div>
|
|
<div>{metadata.get('sender_title', 'Sender Title')}</div>
|
|
</div>
|
|
"""
|
|
|
|
def analyze_letter_tone(content: str) -> Dict[str, float]:
|
|
"""Placeholder: Returns dummy tone analysis."""
|
|
# Returns scores between 0.0 and 1.0
|
|
return {"professional": 0.9, "formal": 0.85, "objective": 0.7}
|
|
|
|
def check_formality(content: str) -> float:
|
|
"""Placeholder: Returns a dummy formality score (0.0 to 1.0)."""
|
|
return 0.88 # Example: 88% formal
|
|
|
|
def get_readability_metrics(content: str) -> Dict[str, Any]:
|
|
"""Placeholder: Returns dummy readability metrics."""
|
|
word_count = len(content.split())
|
|
# Estimate reading time in seconds (assuming ~200 words per minute)
|
|
reading_time_seconds = round((word_count / 200) * 60)
|
|
return {
|
|
"word_count": word_count,
|
|
"sentence_count": max(1, content.count('. ') + content.count('! ') + content.count('? ')), # Simple sentence count
|
|
"avg_words_per_sentence": round(word_count / max(1, content.count('. ') + content.count('! ') + content.count('? ')), 2),
|
|
"flesch_reading_ease": 45.0, # Dummy score for formal letters
|
|
"reading_level": "Difficult", # Dummy level
|
|
"reading_time_seconds": reading_time_seconds # Added reading time
|
|
}
|
|
|
|
def suggest_improvements(content: str, letter_type: str) -> List[str]:
|
|
"""Placeholder: Returns dummy improvement suggestions."""
|
|
if "passive voice" in content.lower():
|
|
return ["Suggestion: Consider using more active voice for clarity and impact."]
|
|
elif len(content.split('.')) < 5:
|
|
return ["Suggestion: The letter seems very short. Ensure all necessary details are included."]
|
|
else:
|
|
return ["Suggestion: Double-check for any jargon that the recipient might not understand."]
|
|
|
|
def get_template_by_type(letter_type: str, subtype: str = "default") -> Dict[str, Any]:
|
|
"""Placeholder: Returns a generic template."""
|
|
# This should ideally come from the actual letter_templates module
|
|
return {"structure": ["Sender Info", "Date", "Recipient Info", "Subject", "Salutation", "Body", "Closing", "Signature"], "guidance": "Follow standard formal letter practices."}
|
|
|
|
def llm_text_gen(prompt: str) -> str:
|
|
"""Placeholder: Simulates LLM text generation."""
|
|
# In a real app, this would call the actual LLM API
|
|
st.info(f"LLM Prompt:\n```\n{prompt}\n```") # Display prompt for debugging
|
|
# Return a dummy generated letter based on the prompt
|
|
return f"Subject: Generated Formal Letter Preview\n\nDear [Generated Recipient Name],\n\nThis is a sample formal letter generated based on the following details:\n\n{prompt}\n\n[Generated content based on the prompt would go here, following the requested structure, formality, tone, and language complexity.]\n\nSincerely,\n[Generated Your Name]"
|
|
|
|
# --- End Placeholder Functions ---
|
|
|
|
|
|
def write_letter():
|
|
"""
|
|
Main function for the Formal Letters interface. Sets up the Streamlit page
|
|
and handles navigation between subtype selection and the letter form.
|
|
"""
|
|
|
|
# Page title and description
|
|
st.title("📝 Formal Letter Writer")
|
|
st.markdown("""
|
|
Create professional formal letters for business, academic, and official purposes. Select a letter type below to get started.
|
|
""")
|
|
|
|
# Initialize Streamlit session state variables specific to the formal module.
|
|
# These variables persist across reruns and store the user's progress and data.
|
|
if "formal_letter_subtype" not in st.session_state:
|
|
st.session_state.formal_letter_subtype = None # Stores the ID of the selected formal letter type
|
|
if "formal_letter_generated" not in st.session_state:
|
|
st.session_state.formal_letter_generated = False # Flag to indicate if a letter has been generated
|
|
if "formal_letter_content" not in st.session_state:
|
|
st.session_state.formal_letter_content = None # Stores the generated letter content
|
|
if "formal_letter_metadata" not in st.session_state:
|
|
st.session_state.formal_letter_metadata = {} # Stores metadata like sender/recipient info
|
|
if "formal_letter_form_data" not in st.session_state:
|
|
st.session_state.formal_letter_form_data = {} # Stores the user's input from the form fields
|
|
|
|
# Back button logic for subtypes. This button appears when a subtype is selected,
|
|
# allowing the user to return to the subtype selection screen.
|
|
if st.session_state.formal_letter_subtype is not None:
|
|
if st.button("← Back to Formal Letter Types"):
|
|
# Reset session state variables for this module to their initial state
|
|
# This clears the current form data and generated letter.
|
|
st.session_state.formal_letter_subtype = None
|
|
st.session_state.formal_letter_generated = False
|
|
st.session_state.formal_letter_content = None
|
|
st.session_state.formal_letter_metadata = {}
|
|
st.session_state.formal_letter_form_data = {}
|
|
st.rerun() # Rerun the app to update the UI based on the changed state
|
|
|
|
# Main navigation logic within the formal module.
|
|
# If no subtype is selected, show the selection grid. Otherwise, show the form for the selected subtype.
|
|
if st.session_state.formal_letter_subtype is None:
|
|
# Display formal letter type selection if no subtype is selected
|
|
display_formal_letter_types()
|
|
else:
|
|
# Display the interface form for the selected formal letter subtype
|
|
display_formal_letter_form(st.session_state.formal_letter_subtype)
|
|
|
|
|
|
def display_formal_letter_types():
|
|
"""
|
|
Displays the formal letter type selection interface using a grid of styled buttons.
|
|
Each button represents a specific type of formal letter the user can choose to write.
|
|
"""
|
|
|
|
st.markdown("## Select Formal Letter Type")
|
|
|
|
# Define formal letter types with their details (ID, Name, Icon, Description, Color)
|
|
# This list is used to generate the selection buttons.
|
|
formal_letter_types = [
|
|
{
|
|
"id": "application",
|
|
"name": "Application Letter",
|
|
"icon": "📋",
|
|
"description": "Apply for a job, program, or opportunity",
|
|
"color": "#1976D2" # Blue
|
|
},
|
|
{
|
|
"id": "complaint",
|
|
"name": "Complaint Letter",
|
|
"icon": "⚠️",
|
|
"description": "Express dissatisfaction with a product or service",
|
|
"color": "#D32F2F" # Red
|
|
},
|
|
{
|
|
"id": "request",
|
|
"name": "Request Letter",
|
|
"icon": "🙋",
|
|
"description": "Make a formal request for information or action",
|
|
"color": "#388E3C" # Green
|
|
},
|
|
{
|
|
"id": "recommendation",
|
|
"name": "Recommendation Letter",
|
|
"icon": "👍",
|
|
"description": "Recommend someone for a position or opportunity",
|
|
"color": "#7B1FA2" # Purple
|
|
},
|
|
{
|
|
"id": "resignation",
|
|
"name": "Resignation Letter",
|
|
"icon": "🚪",
|
|
"description": "Formally resign from a position",
|
|
"color": "#455A64" # Blue Grey
|
|
},
|
|
{
|
|
"id": "inquiry",
|
|
"name": "Inquiry Letter",
|
|
"icon": "❓",
|
|
"description": "Request information about a product, service, or opportunity",
|
|
"color": "#0097A7" # Teal
|
|
},
|
|
{
|
|
"id": "authorization",
|
|
"name": "Authorization Letter",
|
|
"icon": "✅",
|
|
"description": "Grant permission for someone to act on your behalf",
|
|
"color": "#FF5722" # Deep Orange
|
|
},
|
|
{
|
|
"id": "appeal",
|
|
"name": "Appeal Letter",
|
|
"icon": "🔄",
|
|
"description": "Appeal a decision or request reconsideration",
|
|
"color": "#FFA000" # Amber
|
|
},
|
|
{
|
|
"id": "introduction",
|
|
"name": "Introduction Letter",
|
|
"icon": "🤝",
|
|
"description": "Introduce yourself or your organization",
|
|
"color": "#5D4037" # Brown
|
|
}
|
|
]
|
|
|
|
# Inject custom CSS to style the Streamlit buttons to look like cards.
|
|
# This provides a visually appealing selection grid.
|
|
st.markdown("""
|
|
<style>
|
|
/* Target Streamlit buttons and apply card-like styling */
|
|
div.stButton > button {
|
|
width: 100%; /* Make buttons fill their column */
|
|
height: 200px; /* Fixed height for consistent grid */
|
|
border-radius: 10px;
|
|
padding: 20px;
|
|
margin-bottom: 15px; /* Space between rows */
|
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
|
display: flex;
|
|
flex-direction: column;
|
|
justify-content: center;
|
|
align-items: center;
|
|
text-align: center;
|
|
font-size: 16px;
|
|
border: none;
|
|
cursor: pointer;
|
|
transition: transform 0.2s ease-in-out, box-shadow 0.2s ease-in-out;
|
|
color: white !important; /* Ensure text is white */
|
|
}
|
|
/* Hover effect */
|
|
div.stButton > button:hover {
|
|
transform: translateY(-5px); /* Lift effect */
|
|
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.3);
|
|
}
|
|
/* Style for the text inside the button */
|
|
div.stButton > button h3 {
|
|
color: white !important; /* Ensure icon/title is white */
|
|
margin-top: 0;
|
|
margin-bottom: 10px;
|
|
font-size: 1.2em; /* Adjust title size */
|
|
}
|
|
div.stButton > button p {
|
|
color: white !important; /* Ensure description is white */
|
|
font-size: 0.9em; /* Adjust description size */
|
|
margin: 0;
|
|
}
|
|
</style>
|
|
""", unsafe_allow_html=True)
|
|
|
|
|
|
# Create a grid layout for the buttons using Streamlit columns (3 columns per row).
|
|
cols = st.columns(3)
|
|
|
|
# Display each letter type as a button.
|
|
for i, letter_type_config in enumerate(formal_letter_types):
|
|
with cols[i % 3]: # Place buttons in columns, wrapping every 3
|
|
# Use a unique key for each button based on its ID
|
|
# The button label uses markdown and HTML for icon, name, and description
|
|
if st.button(
|
|
f"### {letter_type_config['icon']} {letter_type_config['name']}\n\n<p>{letter_type_config['description']}</p>",
|
|
key=f"btn_formal_select_{letter_type_config['id']}", # Unique key for each button
|
|
unsafe_allow_html=True # Allow markdown and HTML in the button label
|
|
):
|
|
# When a button is clicked, update the session state to the selected subtype ID
|
|
st.session_state.formal_letter_subtype = letter_type_config['id']
|
|
# Clear previous data related to letter generation when selecting a new type
|
|
st.session_state.formal_letter_generated = False
|
|
st.session_state.formal_letter_content = None
|
|
st.session_state.formal_letter_metadata = {}
|
|
st.session_state.formal_letter_form_data = {} # Clear previous form data
|
|
st.rerun() # Rerun the app to switch to the form for the selected subtype
|
|
|
|
# Apply specific background colors to buttons using their keys and custom CSS
|
|
# This requires injecting CSS after the buttons are rendered.
|
|
# Note: This is a common Streamlit workaround for styling individual buttons dynamically.
|
|
button_styles = ""
|
|
for letter_type_config in formal_letter_types:
|
|
button_styles += f"""
|
|
div.stButton > button[data-testid="stButton"][kind="primary"][sf-key*="btn_formal_select_{letter_type_config['id']}"] {{
|
|
background-color: {letter_type_config['color']};
|
|
}}
|
|
div.stButton > button[data-testid="stButton"][kind="primary"][sf-key*="btn_formal_select_{letter_type_config['id']}"]:hover {{
|
|
background-color: {letter_type_config['color']}D9; /* Slightly darker on hover */
|
|
}}
|
|
"""
|
|
st.markdown(f"<style>{button_styles}</style>", unsafe_allow_html=True)
|
|
|
|
|
|
def display_formal_letter_form(subtype: str):
|
|
"""
|
|
Displays the form for the selected formal letter subtype. This includes
|
|
input fields specific to the subtype, contact information fields,
|
|
tone and style options, and tabs for previewing and analyzing the generated letter.
|
|
|
|
Args:
|
|
subtype: The ID string of the selected formal letter subtype.
|
|
"""
|
|
|
|
# Get the template for the selected subtype from the templates module.
|
|
# This provides structural guidance and general advice for the LLM.
|
|
template = get_template_by_type("formal", subtype)
|
|
|
|
# Display the form title, icon, description, and guidance.
|
|
st.markdown(f"## {get_icon_for_subtype(subtype)} {get_name_for_subtype(subtype)}")
|
|
st.markdown(f"*{get_description_for_subtype(subtype)}*")
|
|
st.info(f"**Guidance:** {template.get('guidance', 'No specific guidance available.')}")
|
|
|
|
# Use a Streamlit form to group inputs. This helps manage state and
|
|
# prevents the app from rerunning every time a single input widget changes,
|
|
# improving performance for forms with many inputs.
|
|
with st.form(key=f"formal_letter_form_{subtype}"):
|
|
# Create tabs to organize the form sections.
|
|
tab1, tab2, tab3 = st.tabs(["Letter Details", "Contact Information", "Preview & Export"])
|
|
|
|
# Dictionary to store form data collected from all tabs
|
|
form_data = {}
|
|
|
|
# --- Tab 1: Letter Details ---
|
|
with tab1:
|
|
st.markdown("### Letter Content Details")
|
|
|
|
# Get the configuration for subtype-specific input fields.
|
|
fields = get_fields_for_subtype(subtype)
|
|
|
|
# Create form fields dynamically based on the subtype configuration.
|
|
# Populate default values from session state to retain user input across reruns.
|
|
for field in fields:
|
|
# Retrieve default value from session state, falling back to empty string or specific defaults
|
|
default_value = st.session_state.formal_letter_form_data.get(field["id"], "")
|
|
|
|
# Create the appropriate Streamlit input widget based on the field type.
|
|
# Use a unique key for each widget to ensure state is managed correctly.
|
|
if field["type"] == "text":
|
|
form_data[field["id"]] = st.text_input(field["label"], value=default_value, help=field.get("help", ""), key=f"{subtype}_{field['id']}")
|
|
elif field["type"] == "textarea":
|
|
form_data[field["id"]] = st.text_area(field["label"], value=default_value, help=field.get("help", ""), height=150, key=f"{subtype}_{field['id']}")
|
|
elif field["type"] == "date":
|
|
# Handle date input default value: use stored value if valid, otherwise use today's date.
|
|
try:
|
|
# Attempt to parse stored value as date, fallback to today if unsuccessful
|
|
default_date = datetime.datetime.strptime(str(default_value), "%Y-%m-%d").date() if default_value else datetime.date.today()
|
|
except (ValueError, TypeError):
|
|
default_date = datetime.date.today() # Fallback to today's date
|
|
form_data[field["id"]] = st.date_input(field["label"], value=default_date, help=field.get("help", ""), key=f"{subtype}_{field['id']}")
|
|
elif field["type"] == "select":
|
|
# Determine the index of the default value in the options list.
|
|
try:
|
|
default_index = field["options"].index(default_value) if default_value in field["options"] else 0
|
|
except ValueError:
|
|
default_index = 0 # Default to the first option if the stored value is not valid
|
|
form_data[field["id"]] = st.selectbox(field["label"], field["options"], index=default_index, help=field.get("help", ""), key=f"{subtype}_{field['id']}")
|
|
elif field["type"] == "slider":
|
|
# Use the default value from session state or the field config's default
|
|
default_slider_value = st.session_state.formal_letter_form_data.get(field["id"], field.get("default", (field["min"] + field["max"]) / 2)) # Fallback to midpoint if no default specified
|
|
form_data[field["id"]] = st.slider(field["label"], field["min"], field["max"], default_slider_value, help=field.get("help", ""), key=f"{subtype}_{field['id']}")
|
|
elif field["type"] == "number":
|
|
# Use the default value from session state or the field config's min value
|
|
default_number_value = st.session_state.formal_letter_form_data.get(field["id"], field.get("min", 0))
|
|
form_data[field["id"]] = st.number_input(field["label"], min_value=field.get("min", 0), value=default_number_value, help=field.get("help", ""), key=f"{subtype}_{field['id']}")
|
|
|
|
|
|
# Section for selecting letter tone and style characteristics.
|
|
st.markdown("### Tone and Style")
|
|
col1, col2 = st.columns(2)
|
|
|
|
with col1:
|
|
# Slider for Formality Level, using session state default.
|
|
formality_options = ["Standard Formal", "Very Formal", "Extremely Formal"]
|
|
default_formality_level = st.session_state.formal_letter_form_data.get("formality_level", "Standard Formal")
|
|
form_data["formality_level"] = st.select_slider(
|
|
"Formality Level",
|
|
options=formality_options,
|
|
value=default_formality_level,
|
|
help="Select the desired level of formality for your letter.",
|
|
key=f"{subtype}_formality_level"
|
|
)
|
|
|
|
# Selectbox for Tone, using subtype-specific tones and session state default.
|
|
tone_options = get_tones_for_subtype(subtype)
|
|
default_tone = st.session_state.formal_letter_form_data.get("tone", tone_options[0] if tone_options else "Professional")
|
|
form_data["tone"] = st.selectbox(
|
|
"Tone",
|
|
tone_options,
|
|
index=tone_options.index(default_tone) if default_tone in tone_options else 0,
|
|
help="Select the overall tone for your letter.",
|
|
key=f"{subtype}_tone"
|
|
)
|
|
|
|
with col2:
|
|
# Slider for Length, using session state default.
|
|
length_options = ["Brief", "Standard", "Detailed"]
|
|
default_length = st.session_state.formal_letter_form_data.get("length", "Standard")
|
|
form_data["length"] = st.select_slider(
|
|
"Length",
|
|
options=length_options,
|
|
value=default_length,
|
|
help="Select the desired length of your letter.",
|
|
key=f"{subtype}_length"
|
|
)
|
|
|
|
# Slider for Language Complexity, using session state default.
|
|
complexity_options = ["Simple", "Moderate", "Advanced"]
|
|
default_language_complexity = st.session_state.formal_letter_form_data.get("language_complexity", "Moderate")
|
|
form_data["language_complexity"] = st.select_slider(
|
|
"Language Complexity",
|
|
options=complexity_options,
|
|
value=default_language_complexity,
|
|
help="Select the complexity level of language used in the letter.",
|
|
key=f"{subtype}_language_complexity"
|
|
)
|
|
|
|
# Section for adding additional options like references.
|
|
st.markdown("### Additional Options")
|
|
|
|
# Checkbox and textarea for including references.
|
|
default_include_references = st.session_state.formal_letter_form_data.get("include_references", True)
|
|
include_references = st.checkbox("Include references to relevant documents, policies, or previous communications", value=default_include_references, help="Check to include specific references.", key=f"{subtype}_include_references")
|
|
form_data["references"] = None # Initialize to None
|
|
if include_references:
|
|
default_references = st.session_state.formal_letter_form_data.get("references", "")
|
|
form_data["references"] = st.text_area(
|
|
"References Details",
|
|
value=default_references,
|
|
height=100,
|
|
help="Mention any relevant documents, policies, or previous communications.",
|
|
placeholder="e.g., Regarding your email dated June 15, 2023, about the project timeline...",
|
|
key=f"{subtype}_references"
|
|
)
|
|
|
|
# Advanced options expander for legal/confidentiality notices.
|
|
with st.expander("Advanced Options"):
|
|
# Checkbox and textarea for including a legal disclaimer.
|
|
default_include_legal_disclaimer = st.session_state.formal_letter_form_data.get("include_legal_disclaimer", False)
|
|
include_legal_disclaimer = st.checkbox("Include legal disclaimer", value=default_include_legal_disclaimer, help="Add a legal disclaimer to your letter.", key=f"{subtype}_include_legal_disclaimer")
|
|
form_data["legal_disclaimer"] = None # Initialize to None
|
|
if include_legal_disclaimer:
|
|
default_legal_disclaimer = st.session_state.formal_letter_form_data.get("legal_disclaimer", "")
|
|
form_data["legal_disclaimer"] = st.text_area(
|
|
"Legal Disclaimer Text",
|
|
value=default_legal_disclaimer,
|
|
height=100,
|
|
help="Enter the text for the legal disclaimer.",
|
|
placeholder="e.g., This letter is without prejudice to any rights or remedies available to [Company Name]...",
|
|
key=f"{subtype}_legal_disclaimer"
|
|
)
|
|
|
|
# Checkbox and textarea for including a confidentiality notice.
|
|
default_include_confidentiality_notice = st.session_state.formal_letter_form_data.get("include_confidentiality_notice", False)
|
|
include_confidentiality_notice = st.checkbox("Include confidentiality notice", value=default_include_confidentiality_notice, help="Add a confidentiality notice to your letter.", key=f"{subtype}_include_confidentiality_notice")
|
|
form_data["confidentiality_notice"] = None # Initialize to None
|
|
if include_confidentiality_notice:
|
|
default_confidentiality_notice = st.session_state.formal_letter_form_data.get("confidentiality_notice", "")
|
|
form_data["confidentiality_notice"] = st.text_area(
|
|
"Confidentiality Notice Text",
|
|
value=default_confidentiality_notice,
|
|
height=100,
|
|
help="Enter the text for the confidentiality notice.",
|
|
placeholder="e.g., The information contained in this letter is confidential and intended only for the recipient...",
|
|
key=f"{subtype}_confidentiality_notice"
|
|
)
|
|
|
|
|
|
# --- Tab 2: Contact Information ---
|
|
with tab2:
|
|
# Section for sender and recipient contact information.
|
|
col3, col4 = st.columns(2)
|
|
|
|
with col3:
|
|
st.markdown("### Sender Information")
|
|
# Input fields for sender's contact details, populated from session state.
|
|
form_data["sender_name"] = st.text_input("Your Full Name", value=st.session_state.formal_letter_form_data.get("sender_name", ""), help="Your full name as the sender.", key=f"{subtype}_sender_name")
|
|
form_data["sender_title"] = st.text_input("Your Title/Position", value=st.session_state.formal_letter_form_data.get("sender_title", ""), help="Your job title or position.", key=f"{subtype}_sender_title")
|
|
form_data["sender_organization"] = st.text_input("Your Organization/Company (Optional)", value=st.session_state.formal_letter_form_data.get("sender_organization", ""), help="The name of your organization or company, if applicable.", key=f"{subtype}_sender_organization")
|
|
form_data["sender_address"] = st.text_area("Your Address", value=st.session_state.formal_letter_form_data.get("sender_address", ""), height=100, help="Your full mailing address.", key=f"{subtype}_sender_address")
|
|
form_data["sender_phone"] = st.text_input("Your Phone Number (Optional)", value=st.session_state.formal_letter_form_data.get("sender_phone", ""), help="Your contact phone number.", key=f"{subtype}_sender_phone")
|
|
form_data["sender_email"] = st.text_input("Your Email Address (Optional)", value=st.session_state.formal_letter_form_data.get("sender_email", ""), help="Your contact email address.", key=f"{subtype}_sender_email")
|
|
|
|
with col4:
|
|
st.markdown("### Recipient Information")
|
|
# Input fields for recipient's contact details, populated from session state.
|
|
form_data["recipient_name"] = st.text_input("Recipient's Full Name", value=st.session_state.formal_letter_form_data.get("recipient_name", ""), help="The full name of the recipient (if known).", key=f"{subtype}_recipient_name")
|
|
form_data["recipient_title"] = st.text_input("Recipient's Title/Position (Optional)", value=st.session_state.formal_letter_form_data.get("recipient_title", ""), help="The recipient's job title or position (if known).", key=f"{subtype}_recipient_title")
|
|
form_data["recipient_organization"] = st.text_input("Recipient's Organization/Company", value=st.session_state.formal_letter_form_data.get("recipient_organization", ""), help="The name of the recipient's organization or company.", key=f"{subtype}_recipient_organization")
|
|
form_data["recipient_address"] = st.text_area("Recipient's Address", value=st.session_state.formal_letter_form_data.get("recipient_address", ""), height=100, help="The recipient's full mailing address.", key=f"{subtype}_recipient_address")
|
|
|
|
# Optional recipient contact information in an expander.
|
|
with st.expander("Additional Recipient Information (Optional)"):
|
|
form_data["recipient_phone"] = st.text_input("Recipient's Phone Number (Optional)", value=st.session_state.formal_letter_form_data.get("recipient_phone", ""), help="Recipient's contact phone number.", key=f"{subtype}_recipient_phone")
|
|
form_data["recipient_email"] = st.text_input("Recipient's Email Address (Optional)", value=st.session_state.formal_letter_form_data.get("recipient_email", ""), help="Recipient's contact email address.", key=f"{subtype}_recipient_email")
|
|
|
|
# Section for letter formatting options.
|
|
st.markdown("### Letter Format")
|
|
format_options = ["Full Block", "Modified Block", "Semi-Block"]
|
|
default_letter_format = st.session_state.formal_letter_form_data.get("letter_format", "Full Block")
|
|
form_data["letter_format"] = st.selectbox(
|
|
"Format Style",
|
|
format_options,
|
|
index=format_options.index(default_letter_format) if default_letter_format in format_options else 0,
|
|
help="Select the standard formal letter format style.",
|
|
key=f"{subtype}_letter_format"
|
|
)
|
|
|
|
default_include_subject_line = st.session_state.formal_letter_form_data.get("include_subject_line", True)
|
|
include_subject_line = st.checkbox("Include subject line", value=default_include_subject_line, help="Include a clear subject line.", key=f"{subtype}_include_subject_line")
|
|
form_data["subject_line"] = None # Initialize to None
|
|
if include_subject_line:
|
|
default_subject_line = st.session_state.formal_letter_form_data.get("subject_line", "")
|
|
form_data["subject_line"] = st.text_input(
|
|
"Subject Line Text",
|
|
value=default_subject_line,
|
|
help="Enter the text for the subject line.",
|
|
placeholder="e.g., Application for Marketing Manager Position (Ref: JOB-2023-45)",
|
|
key=f"{subtype}_subject_line"
|
|
)
|
|
|
|
default_include_reference_number = st.session_state.formal_letter_form_data.get("include_reference_number", False)
|
|
include_reference_number = st.checkbox("Include reference number", value=default_include_reference_number, help="Include a reference number for tracking.", key=f"{subtype}_include_reference_number")
|
|
form_data["reference_number"] = None # Initialize to None
|
|
if include_reference_number:
|
|
default_reference_number = st.session_state.formal_letter_form_data.get("reference_number", "")
|
|
form_data["reference_number"] = st.text_input(
|
|
"Reference Number Text",
|
|
value=default_reference_number,
|
|
help="Enter the reference number.",
|
|
placeholder="e.g., REF-2023-123",
|
|
key=f"{subtype}_reference_number"
|
|
)
|
|
|
|
# --- Tab 3: Preview & Export ---
|
|
with tab3:
|
|
# Instructions for the user before generation.
|
|
if not st.session_state.formal_letter_generated:
|
|
st.info("Complete the letter details and click 'Generate Letter' to preview your letter.")
|
|
|
|
# The Generate button is placed inside the form. Clicking it submits the form
|
|
# and triggers the code block below it to run.
|
|
generate_button = st.form_submit_button("Generate Letter", type="primary")
|
|
|
|
if generate_button:
|
|
# Action to perform when the form is submitted via the Generate button.
|
|
|
|
# Store the current state of all form inputs in session state.
|
|
# This allows retaining user inputs even after generation or regeneration.
|
|
st.session_state.formal_letter_form_data = form_data.copy()
|
|
|
|
# Prepare metadata specifically for the formatter and analysis functions.
|
|
# This includes structured contact info, dates, subject, etc.
|
|
metadata = {
|
|
"sender_name": form_data.get("sender_name", ""),
|
|
"sender_title": form_data.get("sender_title", ""),
|
|
"sender_organization": form_data.get("sender_organization", ""),
|
|
"sender_address": form_data.get("sender_address", ""),
|
|
"sender_phone": form_data.get("sender_phone", ""),
|
|
"sender_email": form_data.get("sender_email", ""),
|
|
"recipient_name": form_data.get("recipient_name", ""),
|
|
"recipient_title": form_data.get("recipient_title", ""),
|
|
"recipient_organization": form_data.get("recipient_organization", ""),
|
|
"recipient_address": form_data.get("recipient_address", ""),
|
|
"recipient_phone": form_data.get("recipient_phone", ""),
|
|
"recipient_email": form_data.get("recipient_email", ""),
|
|
"date": datetime.datetime.now().strftime("%B %d, %Y"), # Use current date for the letter
|
|
"letter_format": form_data.get("letter_format", "Full Block"),
|
|
"subject": form_data.get('subject_line') if form_data.get('include_subject_line') else "", # Include subject in metadata for formatter
|
|
"reference_number": form_data.get('reference_number') if form_data.get('include_reference_number') else "", # Include reference in metadata
|
|
}
|
|
# Determine salutation based on recipient name/title preference
|
|
recipient_display_name = metadata.get("recipient_name")
|
|
recipient_display_title = metadata.get("recipient_title")
|
|
if recipient_display_name and recipient_display_title:
|
|
metadata["salutation"] = f"Dear {recipient_display_title} {recipient_display_name}:"
|
|
elif recipient_display_name:
|
|
metadata["salutation"] = f"Dear {recipient_display_name}:"
|
|
else:
|
|
metadata["salutation"] = "Dear Sir/Madam:" # Fallback salutation
|
|
|
|
# Determine complimentary close based on formality
|
|
metadata["complimentary_close"] = "Sincerely," # Standard formal close
|
|
|
|
|
|
st.session_state.formal_letter_metadata = metadata.copy()
|
|
|
|
|
|
# --- Letter Generation Logic ---
|
|
# Check for minimal required fields before attempting generation.
|
|
if not form_data.get("sender_name") or not form_data.get("recipient_name") or not form_data.get("recipient_organization"):
|
|
st.error("Please provide at least your name, the recipient's name, and the recipient's organization.")
|
|
else:
|
|
# Display a spinner while the AI generates the letter.
|
|
with st.spinner("Generating your formal letter..."):
|
|
# Combine all necessary data into a single dictionary for the generation function.
|
|
# This includes both form data and metadata.
|
|
# Note: formality_level, tone, length, language_complexity are already in form_data
|
|
generation_data = {
|
|
"subtype": subtype,
|
|
**form_data, # Includes all collected form inputs
|
|
**metadata # Includes structured sender/recipient/date/format info
|
|
}
|
|
|
|
# Call the letter generation function with the combined data.
|
|
letter_content = generate_formal_letter(generation_data)
|
|
|
|
# Store the generated letter content and update the generated flag.
|
|
st.session_state.formal_letter_content = letter_content
|
|
st.session_state.formal_letter_generated = True
|
|
|
|
# Rerun the app to exit the form block and display the generated letter section.
|
|
# This rerun happens automatically on form submission, but explicit state updates
|
|
# ensure the display logic reacts correctly.
|
|
# st.rerun() # Rerun is handled by form submission
|
|
|
|
# --- Display Generated Letter and Analysis ---
|
|
# This block executes if a letter has been generated and stored in session state.
|
|
if st.session_state.formal_letter_generated and st.session_state.formal_letter_content is not None:
|
|
letter_content = st.session_state.formal_letter_content
|
|
metadata = st.session_state.formal_letter_metadata
|
|
|
|
# Create tabs for different views of the generated letter.
|
|
preview_tab1, preview_tab2, preview_tab3 = st.tabs(["Formatted Preview", "Plain Text", "Analysis"])
|
|
|
|
with preview_tab1:
|
|
st.markdown("### Letter Preview")
|
|
# Generate and display the HTML preview of the letter using the formatter utility.
|
|
# Pass letter_type="formal" to the formatter.
|
|
html_preview = get_letter_preview_html(letter_content, metadata, letter_type="formal")
|
|
st.markdown(html_preview, unsafe_allow_html=True)
|
|
|
|
# Download button for the plain text version of the letter.
|
|
file_name_suffix = metadata.get('recipient_organization', 'formal').replace(' ', '_').lower()
|
|
st.download_button(
|
|
label="Download as Text",
|
|
data=letter_content,
|
|
file_name=f"{subtype}_letter_to_{file_name_suffix}_{datetime.datetime.now().strftime('%Y%m%d')}.txt",
|
|
mime="text/plain"
|
|
)
|
|
|
|
with preview_tab2:
|
|
st.markdown("### Plain Text Content")
|
|
# Display the raw generated letter content in a text area.
|
|
st.text_area("Letter Content", letter_content, height=400, key=f"{subtype}_plain_text_display")
|
|
|
|
# Button to copy the plain text content to the clipboard.
|
|
st.button("Copy Plain Text (Manual Copy from above)", help="Select and copy the text from the box above.", key=f"{subtype}_copy_plain_text_instruction")
|
|
|
|
|
|
with preview_tab3:
|
|
st.markdown("### Letter Analysis")
|
|
# Perform and display analysis of the generated letter using utility functions.
|
|
|
|
# Analyze tone, formality, and readability.
|
|
tone_analysis = analyze_letter_tone(letter_content)
|
|
formality_score = check_formality(letter_content) # Returns score between 0.0 and 1.0
|
|
readability_metrics = get_readability_metrics(letter_content)
|
|
# Get improvement suggestions, passing the letter type for context.
|
|
improvement_suggestions = suggest_improvements(letter_content, "formal") # Pass "formal" as letter_type
|
|
|
|
# Display analysis results in two columns.
|
|
col5, col6 = st.columns(2)
|
|
|
|
with col5:
|
|
st.markdown("#### Tone Analysis")
|
|
# Display each tone score.
|
|
if tone_analysis:
|
|
for tone, score in tone_analysis.items():
|
|
st.write(f"- **{tone.capitalize()}:** {score:.2f}")
|
|
else:
|
|
st.info("Tone analysis not available.")
|
|
|
|
|
|
st.markdown("#### Formality")
|
|
# Display formality score as a percentage and a progress bar.
|
|
st.progress(formality_score) # Progress bar expects a value between 0.0 and 1.0
|
|
st.write(f"Formality Score: {formality_score * 100:.0f}/100") # Display as a percentage (0-100)
|
|
|
|
|
|
with col6:
|
|
st.markdown("#### Readability Metrics")
|
|
# Display various readability metrics.
|
|
if readability_metrics:
|
|
st.write(f"**Word Count:** {readability_metrics.get('word_count', 'N/A')} words")
|
|
st.write(f"**Sentence Count:** {readability_metrics.get('sentence_count', 'N/A')} sentences")
|
|
st.write(f"**Avg Words per Sentence:** {readability_metrics.get('avg_words_per_sentence', 'N/A')}")
|
|
st.write(f"**Flesch Reading Ease:** {readability_metrics.get('flesch_reading_ease', 'N/A')}")
|
|
st.write(f"**Reading Level:** {readability_metrics.get('reading_level', 'N/A')}")
|
|
# Display estimated reading time.
|
|
st.write(f"**Estimated Reading Time:** {readability_metrics.get('reading_time_seconds', 'N/A')} seconds")
|
|
else:
|
|
st.info("Readability metrics not available.")
|
|
|
|
st.markdown("#### Suggestions for Improvement")
|
|
# Display improvement suggestions.
|
|
if improvement_suggestions:
|
|
# Iterate through the list and display each suggestion as a list item.
|
|
for suggestion in improvement_suggestions:
|
|
st.markdown(f"- {suggestion}")
|
|
else:
|
|
st.info("No specific suggestions for improvement found.")
|
|
|
|
# Button to regenerate the letter. Placed outside the form so it's always visible
|
|
# after generation, without needing to resubmit the form first.
|
|
# Keep the form data in session state so the user's inputs are retained.
|
|
if st.button("Regenerate Letter", key=f"{subtype}_regenerate_button"):
|
|
# Reset the generated state and content to allow the form to be displayed again.
|
|
st.session_state.formal_letter_generated = False
|
|
st.session_state.formal_letter_content = None # Clear generated content
|
|
# st.session_state.formal_letter_form_data is already populated from the form submit
|
|
st.rerun() # Rerun to show the form with previous inputs
|
|
|
|
|
|
def generate_formal_letter(data: Dict[str, Any]) -> str:
|
|
"""
|
|
Generates a formal letter using the LLM by constructing a detailed prompt
|
|
based on the collected user inputs and metadata.
|
|
|
|
Args:
|
|
data: A dictionary containing all collected user inputs and metadata
|
|
(from the form and session state).
|
|
|
|
Returns:
|
|
The generated letter content as a string, or an error message if generation fails.
|
|
"""
|
|
|
|
# Extract key generation parameters from the data dictionary.
|
|
subtype = data.get("subtype", "default")
|
|
formality_level = data.get("formality_level", "Standard Formal")
|
|
tone = data.get("tone", "Professional")
|
|
length = data.get("length", "Standard")
|
|
language_complexity = data.get("language_complexity", "Moderate")
|
|
|
|
# Get template guidance and structure to include in the prompt.
|
|
template = get_template_by_type("formal", subtype)
|
|
template_guidance = template.get("guidance", "Follow standard formal letter practices.")
|
|
template_structure = template.get("structure", ["Sender Info", "Date", "Recipient Info", "Subject", "Salutation", "Body", "Closing", "Signature"])
|
|
|
|
|
|
# Build the prompt string step-by-step, including all relevant details
|
|
# from the user's input and selected options.
|
|
prompt_parts = [
|
|
f"Write a {length.lower()}, {formality_level.lower()} {get_name_for_subtype(subtype)} letter with a {tone.lower()} tone using {language_complexity.lower()} language complexity.",
|
|
f"Purpose: {get_description_for_subtype(subtype)}",
|
|
f"Recipient: {data.get('recipient_name', '')}, {data.get('recipient_title', '')} at {data.get('recipient_organization', '')}",
|
|
f"Sender: {data.get('sender_name', '')}, {data.get('sender_title', '')} at {data.get('sender_organization', '')}",
|
|
f"Date: {data.get('date', '')}",
|
|
f"Desired Format Style: {data.get('letter_format', 'Full Block')}",
|
|
]
|
|
|
|
# Add subject line if provided
|
|
if data.get('include_subject_line') and data.get('subject_line'):
|
|
prompt_parts.append(f"Subject: {data['subject_line']}")
|
|
|
|
# Add reference number if provided
|
|
if data.get('include_reference_number') and data.get('reference_number'):
|
|
prompt_parts.append(f"Reference Number: {data['reference_number']}")
|
|
|
|
|
|
# Add subtype-specific details from the collected form data.
|
|
subtype_fields = get_fields_for_subtype(subtype)
|
|
if subtype_fields:
|
|
prompt_parts.append("\nKey Details to Include:")
|
|
for field in subtype_fields:
|
|
field_value = data.get(field["id"])
|
|
# Include the field's label and value in the prompt only if the value is not empty.
|
|
if field_value:
|
|
# Format date fields nicely for the prompt if they are date objects.
|
|
if field["type"] == "date":
|
|
try:
|
|
field_value_str = field_value.strftime("%B %d, %Y")
|
|
except AttributeError:
|
|
field_value_str = str(field_value) # Fallback if not a date object
|
|
else:
|
|
field_value_str = str(field_value)
|
|
|
|
prompt_parts.append(f"- {field['label']}: {field_value_str}")
|
|
|
|
|
|
# Add additional options if included.
|
|
if data.get('include_references') and data.get('references'):
|
|
prompt_parts.append(f"Include references: {data['references']}")
|
|
|
|
if data.get('include_legal_disclaimer') and data.get('legal_disclaimer'):
|
|
prompt_parts.append(f"Include legal disclaimer: {data['legal_disclaimer']}")
|
|
|
|
if data.get('include_confidentiality_notice') and data.get('confidentiality_notice'):
|
|
prompt_parts.append(f"Include confidentiality notice: {data['confidentiality_notice']}")
|
|
|
|
# Add the template structure and overall guidance to the prompt.
|
|
# This helps the LLM understand the desired layout and writing style.
|
|
prompt_parts.append("\nFollow this general structure:")
|
|
for i, section in enumerate(template_structure):
|
|
prompt_parts.append(f"{i+1}. {section}")
|
|
prompt_parts.append(f"\nOverall Writing Guidance: {template_guidance}")
|
|
|
|
# Add final instructions for the LLM.
|
|
prompt_parts.append("\nMake the letter professional, clear, and appropriate for the formal context.")
|
|
|
|
|
|
# Combine all prompt parts into a single string.
|
|
final_prompt = "\n".join(prompt_parts)
|
|
|
|
# Call the LLM text generation function with the constructed prompt.
|
|
try:
|
|
letter_content = llm_text_gen(final_prompt)
|
|
return letter_content
|
|
except Exception as e:
|
|
# Catch any errors during LLM generation and display an error message.
|
|
st.error(f"Error generating letter: {str(e)}")
|
|
return "Error generating letter. Please try again."
|
|
|
|
|
|
# --- Helper functions (from original code, slightly enhanced) ---
|
|
|
|
def get_icon_for_subtype(subtype: str) -> str:
|
|
"""Maps a formal letter subtype ID to a relevant emoji icon."""
|
|
icons = {
|
|
"application": "📋",
|
|
"complaint": "⚠️",
|
|
"request": "🙋",
|
|
"recommendation": "👍",
|
|
"resignation": "🚪",
|
|
"inquiry": "❓",
|
|
"authorization": "✅",
|
|
"appeal": "🔄",
|
|
"introduction": "🤝"
|
|
}
|
|
return icons.get(subtype, "📝") # Default icon
|
|
|
|
def get_name_for_subtype(subtype: str) -> str:
|
|
"""Maps a formal letter subtype ID to its display name."""
|
|
names = {
|
|
"application": "Application Letter",
|
|
"complaint": "Complaint Letter",
|
|
"request": "Request Letter",
|
|
"recommendation": "Recommendation Letter",
|
|
"resignation": "Resignation Letter",
|
|
"inquiry": "Inquiry Letter",
|
|
"authorization": "Authorization Letter",
|
|
"appeal": "Appeal Letter",
|
|
"introduction": "Introduction Letter"
|
|
}
|
|
return names.get(subtype, "Formal Letter") # Default name
|
|
|
|
def get_description_for_subtype(subtype: str) -> str:
|
|
"""Maps a formal letter subtype ID to a brief description."""
|
|
descriptions = {
|
|
"application": "Apply for a job, program, or opportunity with a professional application letter.",
|
|
"complaint": "Express dissatisfaction with a product or service in a formal and effective manner.",
|
|
"request": "Make a formal request for information, assistance, or action.",
|
|
"recommendation": "Recommend someone for a position or opportunity with a professional endorsement.",
|
|
"resignation": "Formally resign from a position while maintaining professional relationships.",
|
|
"inquiry": "Request information about a product, service, or opportunity in a formal manner.",
|
|
"authorization": "Grant permission for someone to act on your behalf with a formal authorization.",
|
|
"appeal": "Appeal a decision or request reconsideration with a persuasive formal letter.",
|
|
"introduction": "Introduce yourself or your organization with a professional introduction letter."
|
|
}
|
|
return descriptions.get(subtype, "Create a formal letter for your specific needs.") # Default description
|
|
|
|
def get_fields_for_subtype(subtype: str) -> List[Dict[str, Any]]:
|
|
"""
|
|
Provides a list of input field configurations specific to each formal letter subtype.
|
|
Each dictionary in the list defines a form input field, including its ID, label,
|
|
type, and optional properties like help text, options (for select/slider),
|
|
min/max values (for slider/number), and a default value.
|
|
"""
|
|
|
|
# Define subtype-specific fields.
|
|
if subtype == "application":
|
|
return [
|
|
{
|
|
"id": "position",
|
|
"label": "Position/Opportunity",
|
|
"type": "text",
|
|
"help": "What specific position, program, or opportunity are you applying for?"
|
|
},
|
|
{
|
|
"id": "source",
|
|
"label": "Where You Found the Opportunity (Optional)",
|
|
"type": "text",
|
|
"help": "Where did you learn about this opportunity? (e.g., company website, job board, referral)"
|
|
},
|
|
{
|
|
"id": "qualifications",
|
|
"label": "Key Qualifications",
|
|
"type": "textarea",
|
|
"help": "List your key qualifications and skills that match the requirements of this position."
|
|
},
|
|
{
|
|
"id": "experience",
|
|
"label": "Relevant Experience",
|
|
"type": "textarea",
|
|
"help": "Describe your relevant work experience, projects, or academic background."
|
|
}
|
|
]
|
|
elif subtype == "complaint":
|
|
return [
|
|
{
|
|
"id": "product_service",
|
|
"label": "Product or Service Involved",
|
|
"type": "text",
|
|
"help": "What product or service is the subject of your complaint?"
|
|
},
|
|
{
|
|
"id": "date_of_incident",
|
|
"label": "Date of Purchase or Incident",
|
|
"type": "date", # Using date type for date input
|
|
"help": "When did you purchase the product or when did the incident occur?"
|
|
},
|
|
{
|
|
"id": "order_reference",
|
|
"label": "Order/Reference Number (Optional)",
|
|
"type": "text",
|
|
"help": "Include any relevant order numbers, account numbers, or reference IDs."
|
|
},
|
|
{
|
|
"id": "complaint_nature",
|
|
"label": "Nature of Complaint",
|
|
"type": "textarea",
|
|
"help": "Describe the issue clearly, factually, and in detail. Include specific dates, times, and names if applicable."
|
|
},
|
|
{
|
|
"id": "desired_resolution",
|
|
"label": "Desired Resolution",
|
|
"type": "textarea",
|
|
"help": "Clearly state what outcome you are seeking to resolve this complaint (e.g., full refund, replacement, repair, specific action)."
|
|
}
|
|
]
|
|
elif subtype == "request":
|
|
return [
|
|
{
|
|
"id": "request_type",
|
|
"label": "Type of Request",
|
|
"type": "text",
|
|
"help": "What type of formal request are you making? (e.g., Request for Information, Request for Meeting, Request for Document)"
|
|
},
|
|
{
|
|
"id": "request_details",
|
|
"label": "Specific Request Details",
|
|
"type": "textarea",
|
|
"help": "Provide all necessary details about what you are requesting."
|
|
},
|
|
{
|
|
"id": "request_reason",
|
|
"label": "Reason or Justification for Request",
|
|
"type": "textarea",
|
|
"help": "Clearly explain why you are making this request and its importance."
|
|
},
|
|
{
|
|
"id": "deadline",
|
|
"label": "Deadline for Response/Action (if applicable)",
|
|
"type": "date", # Using date type for date input
|
|
"help": "Is there a specific date by which you need a response or action?"
|
|
}
|
|
]
|
|
elif subtype == "recommendation":
|
|
return [
|
|
{
|
|
"id": "recommendee",
|
|
"label": "Person Being Recommended (Full Name)",
|
|
"type": "text",
|
|
"help": "Enter the full name of the person you are recommending."
|
|
},
|
|
{
|
|
"id": "position",
|
|
"label": "Position/Opportunity Being Recommended For",
|
|
"type": "text",
|
|
"help": "What specific position, program, or opportunity are you recommending them for?"
|
|
},
|
|
{
|
|
"id": "relationship",
|
|
"label": "Your Relationship to the Recommendee",
|
|
"type": "text",
|
|
"help": "Describe your professional or academic relationship (e.g., former manager, professor, colleague)."
|
|
},
|
|
{
|
|
"id": "relationship_duration",
|
|
"label": "Duration of Relationship",
|
|
"type": "text",
|
|
"help": "How long have you known the person in this capacity? (e.g., 3 years, from 2018 to 2022)"
|
|
},
|
|
{
|
|
"id": "strengths",
|
|
"label": "Key Strengths and Qualities",
|
|
"type": "textarea",
|
|
"help": "Highlight the most relevant strengths and qualities of the person being recommended."
|
|
},
|
|
{
|
|
"id": "achievements",
|
|
"label": "Specific Achievements or Contributions",
|
|
"type": "textarea",
|
|
"help": "Provide concrete examples of their achievements, contributions, or performance."
|
|
}
|
|
]
|
|
elif subtype == "resignation":
|
|
return [
|
|
{
|
|
"id": "current_position",
|
|
"label": "Your Current Position",
|
|
"type": "text",
|
|
"help": "What is your current job title?"
|
|
},
|
|
{
|
|
"id": "last_day",
|
|
"label": "Your Last Working Day",
|
|
"type": "date", # Using date type for date input
|
|
"help": "Specify your intended last day of employment."
|
|
},
|
|
{
|
|
"id": "resignation_reason",
|
|
"label": "Reason for Resignation (Optional)",
|
|
"type": "textarea",
|
|
"help": "You may choose to provide a brief, professional reason for leaving (e.g., pursuing a new opportunity, personal reasons)."
|
|
},
|
|
{
|
|
"id": "transition_plan",
|
|
"label": "Offer of Assistance with Transition (Optional)",
|
|
"type": "textarea",
|
|
"help": "Offer to assist with the transition of your responsibilities."
|
|
},
|
|
{
|
|
"id": "gratitude",
|
|
"label": "Express Gratitude (Optional)",
|
|
"type": "textarea",
|
|
"help": "Express thanks for the opportunity and experience gained."
|
|
}
|
|
]
|
|
elif subtype == "inquiry":
|
|
return [
|
|
{
|
|
"id": "inquiry_subject",
|
|
"label": "Inquiry Subject",
|
|
"type": "text",
|
|
"help": "What is the main subject of your inquiry?"
|
|
},
|
|
{
|
|
"id": "background_info",
|
|
"label": "Relevant Background Information (Optional)",
|
|
"type": "textarea",
|
|
"help": "Provide any necessary context for your inquiry."
|
|
},
|
|
{
|
|
"id": "specific_questions",
|
|
"label": "Specific Questions",
|
|
"type": "textarea",
|
|
"help": "List your questions clearly and concisely, perhaps using bullet points."
|
|
},
|
|
{
|
|
"id": "response_deadline",
|
|
"label": "Deadline for Response (if applicable)",
|
|
"type": "date", # Using date type for date input
|
|
"help": "By when do you need the information?"
|
|
}
|
|
]
|
|
elif subtype == "authorization":
|
|
return [
|
|
{
|
|
"id": "authorized_person",
|
|
"label": "Person Being Authorized (Full Name)",
|
|
"type": "text",
|
|
"help": "Enter the full name of the person you are authorizing."
|
|
},
|
|
{
|
|
"id": "authorized_person_id",
|
|
"label": "Authorized Person's Identification (Optional)",
|
|
"type": "text",
|
|
"help": "Include any identification details if necessary (e.g., ID number, employee ID)."
|
|
},
|
|
{
|
|
"id": "authorization_purpose",
|
|
"label": "Purpose and Scope of Authorization",
|
|
"type": "textarea",
|
|
"help": "Clearly and precisely state what you are authorizing them to do on your behalf."
|
|
},
|
|
{
|
|
"id": "authorization_duration",
|
|
"label": "Duration of Authorization",
|
|
"type": "text", # Using text for flexibility (e.g., "from date to date", "until revoked")
|
|
"help": "Specify how long this authorization is valid (e.g., 'from [Start Date] to [End Date]', 'until revoked in writing')."
|
|
},
|
|
{
|
|
"id": "authorization_limitations",
|
|
"label": "Limitations or Restrictions (Optional)",
|
|
"type": "textarea",
|
|
"help": "Specify any limitations or restrictions on the authorized person's actions."
|
|
}
|
|
]
|
|
elif subtype == "appeal":
|
|
return [
|
|
{
|
|
"id": "appealed_decision",
|
|
"label": "Decision Being Appealed",
|
|
"type": "text",
|
|
"help": "Clearly identify the specific decision you are appealing."
|
|
},
|
|
{
|
|
"id": "decision_date",
|
|
"label": "Date of Original Decision",
|
|
"type": "date", # Using date type for date input
|
|
"help": "When was the original decision made?"
|
|
},
|
|
{
|
|
"id": "appeal_grounds",
|
|
"label": "Grounds for Appeal",
|
|
"type": "textarea",
|
|
"help": "Explain the specific reasons or arguments why you believe the decision should be overturned or reconsidered. Reference relevant policies or facts."
|
|
},
|
|
{
|
|
"id": "supporting_evidence",
|
|
"label": "Supporting Evidence (Optional)",
|
|
"type": "textarea",
|
|
"help": "Mention any supporting documents or evidence you are providing."
|
|
},
|
|
{
|
|
"id": "requested_outcome",
|
|
"label": "Requested Outcome",
|
|
"type": "textarea",
|
|
"help": "Clearly state what resolution you are seeking from this appeal."
|
|
}
|
|
]
|
|
elif subtype == "introduction":
|
|
return [
|
|
{
|
|
"id": "introduction_purpose",
|
|
"label": "Purpose of Introduction",
|
|
"type": "text",
|
|
"help": "Why are you introducing yourself or your organization to this recipient?"
|
|
},
|
|
{
|
|
"id": "key_information",
|
|
"label": "Key Information About Yourself/Organization",
|
|
"type": "textarea",
|
|
"help": "Highlight relevant background, expertise, or services."
|
|
},
|
|
{
|
|
"id": "collaboration_areas",
|
|
"label": "Potential Areas of Collaboration or Mutual Interest (Optional)",
|
|
"type": "textarea",
|
|
"help": "Suggest ways you could potentially collaborate or areas of shared interest."
|
|
},
|
|
{
|
|
"id": "call_to_action",
|
|
"label": "Call to Action",
|
|
"type": "textarea",
|
|
"help": "What specific action would you like the recipient to take after reading your introduction? (e.g., schedule a meeting, visit website)"
|
|
}
|
|
]
|
|
|
|
# Default fields if subtype is not recognized or no specific fields are defined.
|
|
# This provides a basic textarea for general content.
|
|
return [
|
|
{
|
|
"id": "main_content",
|
|
"label": "Main Content",
|
|
"type": "textarea",
|
|
"help": "Enter the main content you want to include in your formal letter."
|
|
}
|
|
]
|
|
|
|
def get_tones_for_subtype(subtype: str) -> List[str]:
|
|
"""Maps a formal letter subtype ID to a list of suggested tones."""
|
|
tones = {
|
|
"application": ["Professional", "Confident", "Enthusiastic", "Respectful", "Formal"],
|
|
"complaint": ["Firm", "Respectful", "Direct", "Objective", "Assertive"],
|
|
"request": ["Polite", "Clear", "Respectful", "Direct", "Appreciative"],
|
|
"recommendation": ["Supportive", "Positive", "Professional", "Enthusiastic", "Confident"],
|
|
"resignation": ["Professional", "Appreciative", "Respectful", "Positive", "Formal"],
|
|
"inquiry": ["Curious", "Professional", "Respectful", "Clear", "Formal"],
|
|
"authorization": ["Clear", "Precise", "Formal", "Direct", "Authoritative"],
|
|
"appeal": ["Persuasive", "Respectful", "Objective", "Confident", "Diplomatic"],
|
|
"introduction": ["Friendly", "Professional", "Enthusiastic", "Informative", "Engaging"]
|
|
}
|
|
# Return the list of tones for the subtype, or a default list if not found.
|
|
return tones.get(subtype, ["Professional", "Formal", "Respectful", "Clear", "Direct"])
|
|
|
|
# Example of how to run the app (for local development using `streamlit run your_script_name.py`)
|
|
# Uncomment the lines below to make this script directly executable.
|
|
# if __name__ == "__main__":
|
|
# write_letter()
|