vimalk78 commited on
Commit
5686111
·
1 Parent(s): 5399a4f

feat: add frontend controls for similarity temperature and difficulty weight

Browse files

- Add AdvancedSettings component with parameter sliders
- Add API support for parameter overrides in puzzle generation
- Pass advanced params through service layers to ThematicWordService
- Include presets for common parameter combinations (Default, Focused, Varied, Challenging)
- Restore original parameter values after generation to prevent side effects

Signed-off-by: Vimal Kumar <vimal78@gmail.com>

crossword-app/backend-py/src/routes/api.py CHANGED
@@ -23,6 +23,8 @@ class GeneratePuzzleRequest(BaseModel):
23
  customSentence: Optional[str] = Field(default=None, description="Optional custom sentence to influence word selection")
24
  multiTheme: bool = Field(default=True, description="Whether to use multi-theme processing or single-theme blending")
25
  wordCount: Optional[int] = Field(default=10, description="Number of words to include in the crossword (8-15)")
 
 
26
 
27
  class WordInfo(BaseModel):
28
  word: str
@@ -130,13 +132,20 @@ async def generate_puzzle(
130
  detail="Word count must be between 8 and 15"
131
  )
132
 
133
- # Generate puzzle
 
 
 
 
 
 
134
  puzzle_data = await crossword_gen.generate_puzzle(
135
  topics=request.topics,
136
  difficulty=request.difficulty,
137
  custom_sentence=request.customSentence,
138
  multi_theme=request.multiTheme,
139
- requested_words=request.wordCount
 
140
  )
141
 
142
  if not puzzle_data:
 
23
  customSentence: Optional[str] = Field(default=None, description="Optional custom sentence to influence word selection")
24
  multiTheme: bool = Field(default=True, description="Whether to use multi-theme processing or single-theme blending")
25
  wordCount: Optional[int] = Field(default=10, description="Number of words to include in the crossword (8-15)")
26
+ similarityTemperature: Optional[float] = Field(default=None, description="Override similarity temperature for word selection randomness (0.1-2.0)")
27
+ difficultyWeight: Optional[float] = Field(default=None, description="Override difficulty weight for similarity vs frequency balance (0.0-1.0)")
28
 
29
  class WordInfo(BaseModel):
30
  word: str
 
132
  detail="Word count must be between 8 and 15"
133
  )
134
 
135
+ # Generate puzzle with optional parameter overrides
136
+ advanced_params = {}
137
+ if request.similarityTemperature is not None:
138
+ advanced_params['similarity_temperature'] = request.similarityTemperature
139
+ if request.difficultyWeight is not None:
140
+ advanced_params['difficulty_weight'] = request.difficultyWeight
141
+
142
  puzzle_data = await crossword_gen.generate_puzzle(
143
  topics=request.topics,
144
  difficulty=request.difficulty,
145
  custom_sentence=request.customSentence,
146
  multi_theme=request.multiTheme,
147
+ requested_words=request.wordCount,
148
+ advanced_params=advanced_params
149
  )
150
 
151
  if not puzzle_data:
crossword-app/backend-py/src/services/crossword_generator.py CHANGED
@@ -18,7 +18,7 @@ class CrosswordGenerator:
18
  self.min_words = 6
19
  self.thematic_service = thematic_service
20
 
21
- async def generate_puzzle(self, topics: List[str], difficulty: str = "medium", custom_sentence: str = None, multi_theme: bool = True, requested_words: int = 10) -> Optional[Dict[str, Any]]:
22
  """
23
  Generate a complete crossword puzzle.
24
  """
@@ -27,7 +27,7 @@ class CrosswordGenerator:
27
  logger.info(f"🎯 Generating puzzle for topics: {topics}, difficulty: {difficulty}{sentence_info}, requested words: {requested_words}")
28
 
29
  # Get words from thematic AI service
30
- words, debug_data = await self._select_words(topics, difficulty, custom_sentence, multi_theme, requested_words)
31
 
32
  if len(words) < self.min_words:
33
  logger.error(f"❌ Not enough words: {len(words)} < {self.min_words}")
@@ -66,7 +66,7 @@ class CrosswordGenerator:
66
  logger.error(f"❌ Error generating puzzle: {e}")
67
  raise
68
 
69
- async def _select_words(self, topics: List[str], difficulty: str, custom_sentence: str = None, multi_theme: bool = True, requested_words: int = 10) -> Tuple[List[Dict[str, Any]], Optional[Dict[str, Any]]]:
70
  """Select words for the crossword using thematic AI service.
71
 
72
  Returns:
@@ -78,7 +78,7 @@ class CrosswordGenerator:
78
  logger.info(f"🎯 Using thematic AI service for word generation with {requested_words} requested words")
79
 
80
  # Use the dedicated crossword method for better word selection
81
- result = await self.thematic_service.find_words_for_crossword(topics, difficulty, requested_words, custom_sentence, multi_theme)
82
 
83
  # Extract words and debug data from new format
84
  words = result["words"]
 
18
  self.min_words = 6
19
  self.thematic_service = thematic_service
20
 
21
+ async def generate_puzzle(self, topics: List[str], difficulty: str = "medium", custom_sentence: str = None, multi_theme: bool = True, requested_words: int = 10, advanced_params: Dict[str, Any] = None) -> Optional[Dict[str, Any]]:
22
  """
23
  Generate a complete crossword puzzle.
24
  """
 
27
  logger.info(f"🎯 Generating puzzle for topics: {topics}, difficulty: {difficulty}{sentence_info}, requested words: {requested_words}")
28
 
29
  # Get words from thematic AI service
30
+ words, debug_data = await self._select_words(topics, difficulty, custom_sentence, multi_theme, requested_words, advanced_params)
31
 
32
  if len(words) < self.min_words:
33
  logger.error(f"❌ Not enough words: {len(words)} < {self.min_words}")
 
66
  logger.error(f"❌ Error generating puzzle: {e}")
67
  raise
68
 
69
+ async def _select_words(self, topics: List[str], difficulty: str, custom_sentence: str = None, multi_theme: bool = True, requested_words: int = 10, advanced_params: Dict[str, Any] = None) -> Tuple[List[Dict[str, Any]], Optional[Dict[str, Any]]]:
70
  """Select words for the crossword using thematic AI service.
71
 
72
  Returns:
 
78
  logger.info(f"🎯 Using thematic AI service for word generation with {requested_words} requested words")
79
 
80
  # Use the dedicated crossword method for better word selection
81
+ result = await self.thematic_service.find_words_for_crossword(topics, difficulty, requested_words, custom_sentence, multi_theme, advanced_params)
82
 
83
  # Extract words and debug data from new format
84
  words = result["words"]
crossword-app/backend-py/src/services/crossword_generator_wrapper.py CHANGED
@@ -23,7 +23,8 @@ class CrosswordGenerator:
23
  difficulty: str = "medium",
24
  custom_sentence: str = None,
25
  multi_theme: bool = True,
26
- requested_words: int = 10
 
27
  ) -> Dict[str, Any]:
28
  """
29
  Generate a complete crossword puzzle using the fixed generator.
@@ -34,6 +35,7 @@ class CrosswordGenerator:
34
  custom_sentence: Optional custom sentence to influence word selection
35
  multi_theme: Whether to use multi-theme processing (True) or single-theme blending (False)
36
  requested_words: Number of words requested by frontend
 
37
 
38
  Returns:
39
  Dictionary containing grid, clues, and metadata
@@ -45,7 +47,7 @@ class CrosswordGenerator:
45
  from .crossword_generator import CrosswordGenerator as ActualGenerator
46
  actual_generator = ActualGenerator(thematic_service=self.thematic_service)
47
 
48
- puzzle = await actual_generator.generate_puzzle(topics, difficulty, custom_sentence, multi_theme, requested_words)
49
 
50
  logger.info(f"✅ Generated crossword with fixed algorithm")
51
  return puzzle
 
23
  difficulty: str = "medium",
24
  custom_sentence: str = None,
25
  multi_theme: bool = True,
26
+ requested_words: int = 10,
27
+ advanced_params: Dict[str, Any] = None
28
  ) -> Dict[str, Any]:
29
  """
30
  Generate a complete crossword puzzle using the fixed generator.
 
35
  custom_sentence: Optional custom sentence to influence word selection
36
  multi_theme: Whether to use multi-theme processing (True) or single-theme blending (False)
37
  requested_words: Number of words requested by frontend
38
+ advanced_params: Optional dict with parameter overrides (similarity_temperature, difficulty_weight)
39
 
40
  Returns:
41
  Dictionary containing grid, clues, and metadata
 
47
  from .crossword_generator import CrosswordGenerator as ActualGenerator
48
  actual_generator = ActualGenerator(thematic_service=self.thematic_service)
49
 
50
+ puzzle = await actual_generator.generate_puzzle(topics, difficulty, custom_sentence, multi_theme, requested_words, advanced_params)
51
 
52
  logger.info(f"✅ Generated crossword with fixed algorithm")
53
  return puzzle
crossword-app/backend-py/src/services/thematic_word_service.py CHANGED
@@ -428,15 +428,74 @@ class ThematicWordService:
428
  # Load or create frequency tiers
429
  self.frequency_tiers = self._create_frequency_tiers()
430
 
431
- # Load model
432
  logger.info(f"🤖 Loading embedding model: {self.model_name}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
433
  model_start = time.time()
434
- self.model = SentenceTransformer(
435
- f'sentence-transformers/{self.model_name}',
436
- cache_folder=str(self.cache_dir)
437
- )
438
- model_time = time.time() - model_start
439
- logger.info(f"✅ Model loaded in {model_time:.2f}s")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
440
 
441
  # Load or create embeddings
442
  self.vocab_embeddings = self._load_or_create_embeddings()
@@ -1440,7 +1499,7 @@ class ThematicWordService:
1440
 
1441
  return status
1442
 
1443
- async def find_words_for_crossword(self, topics: List[str], difficulty: str, requested_words: int = 10, custom_sentence: str = None, multi_theme: bool = True) -> Dict[str, Any]:
1444
  """
1445
  Crossword-specific word finding method with 50% overgeneration and clue quality filtering.
1446
 
@@ -1450,6 +1509,7 @@ class ThematicWordService:
1450
  requested_words: Number of words requested by frontend
1451
  custom_sentence: Optional custom sentence to influence word selection
1452
  multi_theme: Whether to use multi-theme processing (True) or single-theme blending (False)
 
1453
 
1454
  Returns:
1455
  Dictionary with words and optional debug data:
@@ -1484,6 +1544,18 @@ class ThematicWordService:
1484
  thematic_pool = min(self.thematic_pool_size, max(generation_target * 5, 50))
1485
  logger.info(f"🚀 Optimized thematic pool size: {thematic_pool} (was 400) - {((400-thematic_pool)/400*100):.1f}% reduction")
1486
 
 
 
 
 
 
 
 
 
 
 
 
 
1487
  # a result is a tuple of (word, similarity, word_tier)
1488
  raw_results = self.generate_thematic_words(
1489
  input_list,
@@ -1642,6 +1714,15 @@ class ThematicWordService:
1642
  result["debug"] = debug_data
1643
  logger.info(f"🐛 Debug data collected: {len(debug_data['thematic_pool'])} thematic words, {len(debug_data['candidate_words'])} candidates, {len(debug_data['selected_words'])} selected")
1644
 
 
 
 
 
 
 
 
 
 
1645
  return result
1646
 
1647
  def _matches_crossword_difficulty(self, word: str, difficulty: str) -> bool:
 
428
  # Load or create frequency tiers
429
  self.frequency_tiers = self._create_frequency_tiers()
430
 
431
+ # Load model with comprehensive diagnostics
432
  logger.info(f"🤖 Loading embedding model: {self.model_name}")
433
+ logger.info(f"📂 Cache directory: {self.cache_dir}")
434
+ logger.info(f"📂 Cache dir exists: {os.path.exists(self.cache_dir)}")
435
+ if os.path.exists(self.cache_dir):
436
+ logger.info(f"📂 Cache dir writable: {os.access(self.cache_dir, os.W_OK)}")
437
+
438
+ # Check what's in cache
439
+ try:
440
+ cache_contents = os.listdir(self.cache_dir)
441
+ logger.info(f"📂 Cache contents ({len(cache_contents)} items): {cache_contents[:5]}...") # First 5 items
442
+ except Exception as e:
443
+ logger.error(f"❌ Cannot list cache directory: {e}")
444
+
445
+ # Log the exact model path being constructed
446
+ model_path = f'sentence-transformers/{self.model_name}'
447
+ logger.info(f"🔍 Full model path to load: {model_path}")
448
+ logger.info(f"🔍 Model name from env THEMATIC_MODEL_NAME: {os.getenv('THEMATIC_MODEL_NAME')}")
449
+
450
  model_start = time.time()
451
+
452
+ try:
453
+ self.model = SentenceTransformer(
454
+ model_path,
455
+ cache_folder=str(self.cache_dir)
456
+ )
457
+ model_time = time.time() - model_start
458
+ logger.info(f"✅ Model loaded successfully in {model_time:.2f}s")
459
+
460
+ except Exception as e:
461
+ logger.error(f"❌ Failed to load SentenceTransformer model: {e}")
462
+ logger.error(f"🔍 Error type: {type(e).__name__}")
463
+ logger.error(f"🔍 Model name used: {self.model_name}")
464
+ logger.error(f"🔍 Constructed path: {model_path}")
465
+ logger.error(f"🔍 Cache folder: {self.cache_dir}")
466
+
467
+ # Check if model files exist in cache
468
+ model_cache_path = self.cache_dir / "models--sentence-transformers--all-mpnet-base-v2"
469
+ logger.error(f"🔍 Checking model cache path: {model_cache_path}")
470
+
471
+ if model_cache_path.exists():
472
+ logger.error(f"📂 Model cache directory exists")
473
+ try:
474
+ # List files in model cache
475
+ for root, dirs, files in os.walk(model_cache_path):
476
+ rel_path = os.path.relpath(root, model_cache_path)
477
+ if rel_path == ".":
478
+ rel_path = "root"
479
+ logger.error(f" 📁 {rel_path}: {len(files)} files - {files[:3]}...")
480
+ if len(dirs) > 0:
481
+ logger.error(f" 📂 subdirs: {dirs}")
482
+ # Stop after a reasonable number of entries
483
+ if len(files) > 10:
484
+ break
485
+ except Exception as walk_e:
486
+ logger.error(f"❌ Cannot walk model cache: {walk_e}")
487
+ else:
488
+ logger.error(f"📂 Model cache directory does not exist")
489
+
490
+ # Check for any sentence-transformers related folders
491
+ try:
492
+ all_items = os.listdir(self.cache_dir)
493
+ st_items = [item for item in all_items if 'sentence' in item.lower() or 'transform' in item.lower()]
494
+ logger.error(f"📂 SentenceTransformer-related items in cache: {st_items}")
495
+ except Exception as list_e:
496
+ logger.error(f"❌ Cannot check for related items: {list_e}")
497
+
498
+ raise
499
 
500
  # Load or create embeddings
501
  self.vocab_embeddings = self._load_or_create_embeddings()
 
1499
 
1500
  return status
1501
 
1502
+ async def find_words_for_crossword(self, topics: List[str], difficulty: str, requested_words: int = 10, custom_sentence: str = None, multi_theme: bool = True, advanced_params: Dict[str, Any] = None) -> Dict[str, Any]:
1503
  """
1504
  Crossword-specific word finding method with 50% overgeneration and clue quality filtering.
1505
 
 
1509
  requested_words: Number of words requested by frontend
1510
  custom_sentence: Optional custom sentence to influence word selection
1511
  multi_theme: Whether to use multi-theme processing (True) or single-theme blending (False)
1512
+ advanced_params: Optional dict with parameter overrides (similarity_temperature, difficulty_weight)
1513
 
1514
  Returns:
1515
  Dictionary with words and optional debug data:
 
1544
  thematic_pool = min(self.thematic_pool_size, max(generation_target * 5, 50))
1545
  logger.info(f"🚀 Optimized thematic pool size: {thematic_pool} (was 400) - {((400-thematic_pool)/400*100):.1f}% reduction")
1546
 
1547
+ # Handle advanced parameter overrides
1548
+ original_temp = self.similarity_temperature
1549
+ original_weight = self.difficulty_weight
1550
+
1551
+ if advanced_params:
1552
+ if 'similarity_temperature' in advanced_params:
1553
+ self.similarity_temperature = advanced_params['similarity_temperature']
1554
+ logger.info(f"🎛️ Overriding similarity temperature: {original_temp} → {self.similarity_temperature}")
1555
+ if 'difficulty_weight' in advanced_params:
1556
+ self.difficulty_weight = advanced_params['difficulty_weight']
1557
+ logger.info(f"🎛️ Overriding difficulty weight: {original_weight} → {self.difficulty_weight}")
1558
+
1559
  # a result is a tuple of (word, similarity, word_tier)
1560
  raw_results = self.generate_thematic_words(
1561
  input_list,
 
1714
  result["debug"] = debug_data
1715
  logger.info(f"🐛 Debug data collected: {len(debug_data['thematic_pool'])} thematic words, {len(debug_data['candidate_words'])} candidates, {len(debug_data['selected_words'])} selected")
1716
 
1717
+ # Restore original parameter values if they were overridden
1718
+ if advanced_params:
1719
+ self.similarity_temperature = original_temp
1720
+ self.difficulty_weight = original_weight
1721
+ if 'similarity_temperature' in advanced_params:
1722
+ logger.info(f"🔄 Restored similarity temperature: {self.similarity_temperature}")
1723
+ if 'difficulty_weight' in advanced_params:
1724
+ logger.info(f"🔄 Restored difficulty weight: {self.difficulty_weight}")
1725
+
1726
  return result
1727
 
1728
  def _matches_crossword_difficulty(self, word: str, difficulty: str) -> bool:
crossword-app/frontend/src/App.jsx CHANGED
@@ -3,6 +3,7 @@ import TopicSelector from './components/TopicSelector';
3
  import PuzzleGrid from './components/PuzzleGrid';
4
  import ClueList from './components/ClueList';
5
  import DebugTab from './components/DebugTab';
 
6
  import LoadingSpinner from './components/LoadingSpinner';
7
  import useCrossword from './hooks/useCrossword';
8
  import './styles/puzzle.css';
@@ -16,6 +17,10 @@ function App() {
16
  const [multiTheme, setMultiTheme] = useState(true);
17
  const [activeTab, setActiveTab] = useState('puzzle');
18
 
 
 
 
 
19
  const {
20
  puzzle,
21
  loading,
@@ -40,7 +45,10 @@ function App() {
40
  }
41
 
42
  setShowSolution(false);
43
- await generatePuzzle(allTopics, difficulty, false, customSentence, multiTheme);
 
 
 
44
  };
45
 
46
  const handleTopicsChange = (topics) => {
@@ -69,6 +77,8 @@ function App() {
69
  setCustomSentence('');
70
  setMultiTheme(true);
71
  setActiveTab('puzzle');
 
 
72
  };
73
 
74
  const handleRevealSolution = () => {
@@ -94,6 +104,13 @@ function App() {
94
  onMultiThemeChange={handleMultiThemeChange}
95
  />
96
 
 
 
 
 
 
 
 
97
  <div className="puzzle-controls">
98
  <select
99
  value={difficulty}
 
3
  import PuzzleGrid from './components/PuzzleGrid';
4
  import ClueList from './components/ClueList';
5
  import DebugTab from './components/DebugTab';
6
+ import AdvancedSettings from './components/AdvancedSettings';
7
  import LoadingSpinner from './components/LoadingSpinner';
8
  import useCrossword from './hooks/useCrossword';
9
  import './styles/puzzle.css';
 
17
  const [multiTheme, setMultiTheme] = useState(true);
18
  const [activeTab, setActiveTab] = useState('puzzle');
19
 
20
+ // Advanced settings state
21
+ const [similarityTemperature, setSimilarityTemperature] = useState(0.2);
22
+ const [difficultyWeight, setDifficultyWeight] = useState(0.5);
23
+
24
  const {
25
  puzzle,
26
  loading,
 
45
  }
46
 
47
  setShowSolution(false);
48
+ await generatePuzzle(allTopics, difficulty, false, customSentence, multiTheme, {
49
+ similarityTemperature,
50
+ difficultyWeight
51
+ });
52
  };
53
 
54
  const handleTopicsChange = (topics) => {
 
77
  setCustomSentence('');
78
  setMultiTheme(true);
79
  setActiveTab('puzzle');
80
+ setSimilarityTemperature(0.2);
81
+ setDifficultyWeight(0.5);
82
  };
83
 
84
  const handleRevealSolution = () => {
 
104
  onMultiThemeChange={handleMultiThemeChange}
105
  />
106
 
107
+ <AdvancedSettings
108
+ similarityTemperature={similarityTemperature}
109
+ onTemperatureChange={setSimilarityTemperature}
110
+ difficultyWeight={difficultyWeight}
111
+ onWeightChange={setDifficultyWeight}
112
+ />
113
+
114
  <div className="puzzle-controls">
115
  <select
116
  value={difficulty}
crossword-app/frontend/src/components/AdvancedSettings.jsx ADDED
@@ -0,0 +1,155 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState } from 'react';
2
+
3
+ const AdvancedSettings = ({
4
+ similarityTemperature,
5
+ onTemperatureChange,
6
+ difficultyWeight,
7
+ onWeightChange
8
+ }) => {
9
+ const [isExpanded, setIsExpanded] = useState(false);
10
+
11
+ const presets = [
12
+ {
13
+ name: 'Default',
14
+ temp: 0.2,
15
+ weight: 0.5,
16
+ description: 'Balanced settings (recommended)'
17
+ },
18
+ {
19
+ name: 'Focused',
20
+ temp: 0.1,
21
+ weight: 0.3,
22
+ description: 'Deterministic, similarity-based'
23
+ },
24
+ {
25
+ name: 'Varied',
26
+ temp: 0.7,
27
+ weight: 0.5,
28
+ description: 'More randomness and variety'
29
+ },
30
+ {
31
+ name: 'Challenging',
32
+ temp: 0.5,
33
+ weight: 0.7,
34
+ description: 'Respects difficulty levels more'
35
+ }
36
+ ];
37
+
38
+ const applyPreset = (preset) => {
39
+ onTemperatureChange(preset.temp);
40
+ onWeightChange(preset.weight);
41
+ };
42
+
43
+ const getTemperatureDescription = (value) => {
44
+ if (value <= 0.3) return 'Deterministic (top similar words)';
45
+ if (value <= 0.6) return 'Balanced (good variety)';
46
+ if (value <= 1.0) return 'Random (surprising choices)';
47
+ return 'Very random (unpredictable)';
48
+ };
49
+
50
+ const getWeightDescription = (value) => {
51
+ if (value <= 0.2) return 'Similarity-focused';
52
+ if (value <= 0.4) return 'Slightly similarity-focused';
53
+ if (value <= 0.6) return 'Balanced';
54
+ if (value <= 0.8) return 'Slightly frequency-focused';
55
+ return 'Frequency-focused';
56
+ };
57
+
58
+ return (
59
+ <div className="advanced-settings">
60
+ <div
61
+ className="advanced-settings-header"
62
+ onClick={() => setIsExpanded(!isExpanded)}
63
+ >
64
+ <span className="settings-icon">⚙️</span>
65
+ <span className="settings-title">Advanced Settings</span>
66
+ <span className="expand-icon">{isExpanded ? '▼' : '▶'}</span>
67
+ </div>
68
+
69
+ {isExpanded && (
70
+ <div className="advanced-settings-content">
71
+ <div className="setting-group">
72
+ <label className="setting-label">
73
+ Word Selection Randomness
74
+ <span className="setting-value">{similarityTemperature.toFixed(1)}</span>
75
+ </label>
76
+ <div className="slider-container">
77
+ <input
78
+ type="range"
79
+ min="0.1"
80
+ max="2.0"
81
+ step="0.1"
82
+ value={similarityTemperature}
83
+ onChange={(e) => onTemperatureChange(parseFloat(e.target.value))}
84
+ className="slider"
85
+ />
86
+ <div className="slider-labels">
87
+ <span>More focused</span>
88
+ <span>More variety</span>
89
+ </div>
90
+ </div>
91
+ <div className="setting-description">
92
+ {getTemperatureDescription(similarityTemperature)}
93
+ </div>
94
+ </div>
95
+
96
+ <div className="setting-group">
97
+ <label className="setting-label">
98
+ Difficulty Balance
99
+ <span className="setting-value">{difficultyWeight.toFixed(1)}</span>
100
+ </label>
101
+ <div className="slider-container">
102
+ <input
103
+ type="range"
104
+ min="0.0"
105
+ max="1.0"
106
+ step="0.1"
107
+ value={difficultyWeight}
108
+ onChange={(e) => onWeightChange(parseFloat(e.target.value))}
109
+ className="slider"
110
+ />
111
+ <div className="slider-labels">
112
+ <span>Similarity</span>
113
+ <span>Frequency</span>
114
+ </div>
115
+ </div>
116
+ <div className="setting-description">
117
+ {getWeightDescription(difficultyWeight)}
118
+ </div>
119
+ </div>
120
+
121
+ <div className="presets-section">
122
+ <div className="presets-label">Quick Presets:</div>
123
+ <div className="presets-container">
124
+ {presets.map((preset, index) => (
125
+ <button
126
+ key={index}
127
+ onClick={() => applyPreset(preset)}
128
+ className={`preset-button ${
129
+ similarityTemperature === preset.temp && difficultyWeight === preset.weight
130
+ ? 'active'
131
+ : ''
132
+ }`}
133
+ title={preset.description}
134
+ >
135
+ {preset.name}
136
+ </button>
137
+ ))}
138
+ </div>
139
+ </div>
140
+
141
+ <div className="settings-info">
142
+ <div className="info-item">
143
+ <strong>Temperature:</strong> Controls how random word selection is. Lower values pick the most similar words, higher values add variety.
144
+ </div>
145
+ <div className="info-item">
146
+ <strong>Weight:</strong> Balances similarity vs word frequency. Lower focuses on semantic similarity, higher respects difficulty levels.
147
+ </div>
148
+ </div>
149
+ </div>
150
+ )}
151
+ </div>
152
+ );
153
+ };
154
+
155
+ export default AdvancedSettings;
crossword-app/frontend/src/hooks/useCrossword.js CHANGED
@@ -22,7 +22,7 @@ const useCrossword = () => {
22
  }
23
  }, [API_BASE_URL]);
24
 
25
- const generatePuzzle = useCallback(async (selectedTopics, difficulty = 'medium', useAI = false, customSentence = '', multiTheme = true) => {
26
  try {
27
  setLoading(true);
28
  setError(null);
@@ -37,7 +37,13 @@ const useCrossword = () => {
37
  difficulty,
38
  useAI,
39
  ...(customSentence && { customSentence }),
40
- multiTheme
 
 
 
 
 
 
41
  })
42
  });
43
 
 
22
  }
23
  }, [API_BASE_URL]);
24
 
25
+ const generatePuzzle = useCallback(async (selectedTopics, difficulty = 'medium', useAI = false, customSentence = '', multiTheme = true, advancedParams = {}) => {
26
  try {
27
  setLoading(true);
28
  setError(null);
 
37
  difficulty,
38
  useAI,
39
  ...(customSentence && { customSentence }),
40
+ multiTheme,
41
+ ...(advancedParams.similarityTemperature !== undefined && {
42
+ similarityTemperature: advancedParams.similarityTemperature
43
+ }),
44
+ ...(advancedParams.difficultyWeight !== undefined && {
45
+ difficultyWeight: advancedParams.difficultyWeight
46
+ })
47
  })
48
  });
49
 
crossword-app/frontend/src/styles/puzzle.css CHANGED
@@ -1,5 +1,184 @@
1
  /* Crossword Puzzle Styles */
2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  .crossword-app {
4
  max-width: 1200px;
5
  margin: 0 auto;
 
1
  /* Crossword Puzzle Styles */
2
 
3
+ /* Advanced Settings Styles */
4
+ .advanced-settings {
5
+ background: #f8f9fa;
6
+ border: 1px solid #dee2e6;
7
+ border-radius: 8px;
8
+ margin-bottom: 20px;
9
+ overflow: hidden;
10
+ }
11
+
12
+ .advanced-settings-header {
13
+ display: flex;
14
+ align-items: center;
15
+ padding: 12px 16px;
16
+ background: #e9ecef;
17
+ cursor: pointer;
18
+ user-select: none;
19
+ transition: background-color 0.2s ease;
20
+ }
21
+
22
+ .advanced-settings-header:hover {
23
+ background: #dee2e6;
24
+ }
25
+
26
+ .settings-icon {
27
+ margin-right: 8px;
28
+ font-size: 1.1rem;
29
+ }
30
+
31
+ .settings-title {
32
+ flex: 1;
33
+ font-weight: 600;
34
+ color: #2c3e50;
35
+ }
36
+
37
+ .expand-icon {
38
+ color: #6c757d;
39
+ font-size: 0.9rem;
40
+ }
41
+
42
+ .advanced-settings-content {
43
+ padding: 20px;
44
+ }
45
+
46
+ .setting-group {
47
+ margin-bottom: 24px;
48
+ }
49
+
50
+ .setting-group:last-child {
51
+ margin-bottom: 0;
52
+ }
53
+
54
+ .setting-label {
55
+ display: flex;
56
+ justify-content: space-between;
57
+ align-items: center;
58
+ font-weight: 600;
59
+ color: #2c3e50;
60
+ margin-bottom: 8px;
61
+ }
62
+
63
+ .setting-value {
64
+ background: #e9ecef;
65
+ padding: 2px 8px;
66
+ border-radius: 12px;
67
+ font-size: 0.9rem;
68
+ color: #495057;
69
+ }
70
+
71
+ .slider-container {
72
+ margin-bottom: 8px;
73
+ }
74
+
75
+ .slider {
76
+ width: 100%;
77
+ height: 6px;
78
+ border-radius: 3px;
79
+ background: #dee2e6;
80
+ outline: none;
81
+ -webkit-appearance: none;
82
+ cursor: pointer;
83
+ }
84
+
85
+ .slider::-webkit-slider-thumb {
86
+ -webkit-appearance: none;
87
+ appearance: none;
88
+ width: 20px;
89
+ height: 20px;
90
+ border-radius: 50%;
91
+ background: #3498db;
92
+ cursor: pointer;
93
+ transition: transform 0.2s ease;
94
+ }
95
+
96
+ .slider::-webkit-slider-thumb:hover {
97
+ transform: scale(1.1);
98
+ }
99
+
100
+ .slider::-moz-range-thumb {
101
+ width: 20px;
102
+ height: 20px;
103
+ border-radius: 50%;
104
+ background: #3498db;
105
+ cursor: pointer;
106
+ border: none;
107
+ }
108
+
109
+ .slider-labels {
110
+ display: flex;
111
+ justify-content: space-between;
112
+ margin-top: 4px;
113
+ font-size: 0.85rem;
114
+ color: #6c757d;
115
+ }
116
+
117
+ .setting-description {
118
+ font-size: 0.9rem;
119
+ color: #6c757d;
120
+ font-style: italic;
121
+ }
122
+
123
+ .presets-section {
124
+ margin-top: 20px;
125
+ padding-top: 20px;
126
+ border-top: 1px solid #dee2e6;
127
+ }
128
+
129
+ .presets-label {
130
+ font-weight: 600;
131
+ color: #2c3e50;
132
+ margin-bottom: 10px;
133
+ }
134
+
135
+ .presets-container {
136
+ display: flex;
137
+ gap: 8px;
138
+ flex-wrap: wrap;
139
+ }
140
+
141
+ .preset-button {
142
+ padding: 6px 12px;
143
+ border: 2px solid #6c757d;
144
+ background: white;
145
+ color: #6c757d;
146
+ border-radius: 16px;
147
+ cursor: pointer;
148
+ transition: all 0.2s ease;
149
+ font-size: 0.85rem;
150
+ font-weight: 500;
151
+ }
152
+
153
+ .preset-button:hover {
154
+ border-color: #3498db;
155
+ color: #3498db;
156
+ }
157
+
158
+ .preset-button.active {
159
+ border-color: #3498db;
160
+ background: #3498db;
161
+ color: white;
162
+ }
163
+
164
+ .settings-info {
165
+ margin-top: 16px;
166
+ padding: 12px;
167
+ background: #e3f2fd;
168
+ border-radius: 6px;
169
+ border-left: 4px solid #2196f3;
170
+ }
171
+
172
+ .info-item {
173
+ margin-bottom: 8px;
174
+ font-size: 0.85rem;
175
+ color: #1976d2;
176
+ }
177
+
178
+ .info-item:last-child {
179
+ margin-bottom: 0;
180
+ }
181
+
182
  .crossword-app {
183
  max-width: 1200px;
184
  margin: 0 auto;