added custom puzzle page
Browse files- backend/Example.txt +7 -7
- backend/Example_multi.txt +164 -0
- backend/app.py +20 -1
- backend/solver.py +82 -0
- frontend/src/App.css +278 -0
- frontend/src/App.js +30 -292
- frontend/src/components/SolutionNavigator.js +102 -0
- frontend/src/pages/CustomPuzzlePage.js +321 -0
- frontend/src/pages/PredefinedPuzzlePage.js +304 -0
backend/Example.txt
CHANGED
@@ -153,13 +153,13 @@ if s.check() == sat:
|
|
153 |
block.append(Vars[cat_idx][i] != m[Vars[cat_idx][i]])
|
154 |
s.add(Or(block))
|
155 |
while s.check() == sat:
|
156 |
-
|
157 |
-
|
158 |
-
|
159 |
-
|
160 |
-
|
161 |
-
|
162 |
-
|
163 |
|
164 |
if cnt == 1:
|
165 |
# Output the solution as a JSON string.
|
|
|
153 |
block.append(Vars[cat_idx][i] != m[Vars[cat_idx][i]])
|
154 |
s.add(Or(block))
|
155 |
while s.check() == sat:
|
156 |
+
m = s.model()
|
157 |
+
cnt += 1
|
158 |
+
block = []
|
159 |
+
for cat_idx, (cat_name, item_list) in enumerate(categories):
|
160 |
+
for i in range(NUM_POSITIONS):
|
161 |
+
block.append(Vars[cat_idx][i] != m[Vars[cat_idx][i]])
|
162 |
+
s.add(Or(block))
|
163 |
|
164 |
if cnt == 1:
|
165 |
# Output the solution as a JSON string.
|
backend/Example_multi.txt
ADDED
@@ -0,0 +1,164 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
This is an example of representing a Zebra Puzzle using a Z3-based Python code:
|
2 |
+
|
3 |
+
There are 5 Producers in this puzzle.
|
4 |
+
|
5 |
+
Categories and possible items:
|
6 |
+
- Shirt: black, green, orange, pink, red
|
7 |
+
- Name: Barbara, Isabella, Jane, Nicole, Rachel
|
8 |
+
- Jam: apricot, cherry, fig, raspberry, watermelon
|
9 |
+
- Size: 10 oz, 12 oz, 14 oz, 20 oz, 6 oz
|
10 |
+
- Source: backyard, farmers' coop, local grocer, organic farm, wild pickings
|
11 |
+
|
12 |
+
Clues:
|
13 |
+
- The producer whose jam comes from Wild pickings is in the fourth position.
|
14 |
+
- The producer selling Cherry jam is somewhere to the right of the one wearing a Green shirt.
|
15 |
+
- Isabella is selling Cherry jam.
|
16 |
+
- The producer wearing a Pink shirt is somewhere between the producer whose jam source is the Backyard and the one selling Watermelon jam, in that order.
|
17 |
+
- The producer selling 14 oz jars is somewhere to the right of the one wearing a Green shirt.
|
18 |
+
- The producer with 14 oz jars is next to the one selling Raspberry jam.
|
19 |
+
- The Raspberry jam producer is positioned at one of the ends.
|
20 |
+
- Barbara is next to the producer whose jam source is the Organic farm.
|
21 |
+
- Jane is located somewhere between Nicole and Isabella, in that order.
|
22 |
+
- The producer of Fig jam sources the fruit from an Organic farm.
|
23 |
+
- The producer with 10 oz jars is at one of the ends.
|
24 |
+
- The Raspberry jam producer is somewhere to the right of the one wearing a Green shirt.
|
25 |
+
- The producer with 12 oz jars is next to the one who has 6 oz jars.
|
26 |
+
- Isabella is next to the producer wearing a Black shirt.
|
27 |
+
- The producer with 6 oz jars is next to the one whose source is the Local grocer.
|
28 |
+
- The producer with 6 oz jars is in the second position.
|
29 |
+
- Rachel's source of fruit is the Farmers' coop.
|
30 |
+
- Barbara is next to Nicole.
|
31 |
+
- The producer wearing an Orange shirt gets her fruit from the Backyard.
|
32 |
+
- The producer with 12 oz jars is in the very first position.
|
33 |
+
|
34 |
+
```python
|
35 |
+
from z3 import *
|
36 |
+
import json
|
37 |
+
import sys
|
38 |
+
|
39 |
+
# Define all categories in a single list of tuples:
|
40 |
+
# (House is implicit; each row's first column will be the house number.)
|
41 |
+
categories = [
|
42 |
+
("Shirt", ["black", "green", "orange", "pink", "red"]),
|
43 |
+
("Name", ["Barbara", "Isabella", "Jane", "Nicole", "Rachel"]),
|
44 |
+
("Jam", ["apricot", "cherry", "fig", "raspberry", "watermelon"]),
|
45 |
+
("Size", ["10 oz", "12 oz", "14 oz", "20 oz", "6 oz"]),
|
46 |
+
("Source", ["backyard", "farmers' coop", "local grocer", "organic farm", "wild pickings"])
|
47 |
+
]
|
48 |
+
|
49 |
+
# No need to change here, automatically processing
|
50 |
+
NUM_POSITIONS = len(categories[0][1])
|
51 |
+
|
52 |
+
item_to_cat_and_index = {}
|
53 |
+
for cat_idx, (cat_str, item_list) in enumerate(categories):
|
54 |
+
for item_idx, item_str in enumerate(item_list):
|
55 |
+
item_to_cat_and_index[(cat_str, item_str)] = (cat_idx, item_idx)
|
56 |
+
|
57 |
+
Vars = []
|
58 |
+
for cat_idx, (cat_name, item_list) in enumerate(categories):
|
59 |
+
var = IntVector(cat_name, len(item_list))
|
60 |
+
Vars.append(var)
|
61 |
+
|
62 |
+
s = Solver()
|
63 |
+
|
64 |
+
for cat_idx, (cat_name, item_list) in enumerate(categories):
|
65 |
+
for item_idx, item_str in enumerate(item_list):
|
66 |
+
s.add(Vars[cat_idx][item_idx] >= 1, Vars[cat_idx][item_idx] <= NUM_POSITIONS)
|
67 |
+
s.add(Distinct(Vars[cat_idx]))
|
68 |
+
|
69 |
+
def pos(cat_str, item_str):
|
70 |
+
(cat_idx, item_idx) = item_to_cat_and_index[(cat_str, item_str)]
|
71 |
+
return Vars[cat_idx][item_idx]
|
72 |
+
|
73 |
+
# All clues here
|
74 |
+
# The producer whose jam comes from Wild pickings is in the fourth position.
|
75 |
+
s.add(pos("Source", "wild pickings") == 4)
|
76 |
+
|
77 |
+
# The producer selling Cherry jam is somewhere to the right of the one wearing a Green shirt.
|
78 |
+
s.add(pos("Jam", "cherry") > pos("Shirt", "green"))
|
79 |
+
|
80 |
+
# Isabella is selling Cherry jam.
|
81 |
+
s.add(pos("Name", "Isabella") == pos("Jam", "cherry"))
|
82 |
+
|
83 |
+
# The producer wearing a Pink shirt is somewhere between the producer whose jam source is the Backyard and the one selling Watermelon jam, in that order.
|
84 |
+
s.add(And(pos("Source", "backyard") < pos("Shirt", "pink"), pos("Shirt", "pink") < pos("Jam", "watermelon")))
|
85 |
+
|
86 |
+
# The producer selling 14 oz jars is somewhere to the right of the one wearing a Green shirt.
|
87 |
+
s.add(pos("Size", "14 oz") > pos("Shirt", "green"))
|
88 |
+
|
89 |
+
# The producer with 14 oz jars is next to the one selling Raspberry jam.
|
90 |
+
s.add(Abs(pos("Size", "14 oz") - pos("Jam", "raspberry")) == 1)
|
91 |
+
|
92 |
+
# The Raspberry jam producer is positioned at one of the ends.
|
93 |
+
s.add(Or(pos("Jam", "raspberry") == 1, pos("Jam", "raspberry") == NUM_POSITIONS))
|
94 |
+
|
95 |
+
# Barbara is next to the producer whose jam source is the Organic farm.
|
96 |
+
s.add(Abs(pos("Name", "Barbara") - pos("Source", "organic farm")) == 1)
|
97 |
+
|
98 |
+
# Jane is located somewhere between Nicole and Isabella, in that order.
|
99 |
+
s.add(And(pos("Name", "Nicole") < pos("Name", "Jane"), pos("Name", "Jane") < pos("Name", "Isabella")))
|
100 |
+
|
101 |
+
# The producer of Fig jam sources the fruit from an Organic farm.
|
102 |
+
s.add(pos("Jam", "fig") == pos("Source", "organic farm"))
|
103 |
+
|
104 |
+
# The producer with 10 oz jars is at one of the ends.
|
105 |
+
s.add(Or(pos("Size", "10 oz") == 1, pos("Size", "10 oz") == NUM_POSITIONS))
|
106 |
+
|
107 |
+
# The Raspberry jam producer is somewhere to the right of the one wearing a Green shirt.
|
108 |
+
s.add(pos("Jam", "raspberry") > pos("Shirt", "green"))
|
109 |
+
|
110 |
+
# The producer with 12 oz jars is next to the one who has 6 oz jars.
|
111 |
+
s.add(Abs(pos("Size", "12 oz") - pos("Size", "6 oz")) == 1)
|
112 |
+
|
113 |
+
# Isabella is next to the producer wearing a Black shirt.
|
114 |
+
s.add(Abs(pos("Name", "Isabella") - pos("Shirt", "black")) == 1)
|
115 |
+
|
116 |
+
# The producer with 6 oz jars is next to the one whose source is the Local grocer.
|
117 |
+
s.add(Abs(pos("Size", "6 oz") - pos("Source", "local grocer")) == 1)
|
118 |
+
|
119 |
+
# The producer with 6 oz jars is in the second position.
|
120 |
+
s.add(pos("Size", "6 oz") == 2)
|
121 |
+
|
122 |
+
# Rachel's source of fruit is the Farmers' coop.
|
123 |
+
s.add(pos("Name", "Rachel") == pos("Source", "farmers' coop"))
|
124 |
+
|
125 |
+
# Barbara is next to Nicole.
|
126 |
+
s.add(Abs(pos("Name", "Barbara") - pos("Name", "Nicole")) == 1)
|
127 |
+
|
128 |
+
# The producer wearing an Orange shirt gets her fruit from the Backyard.
|
129 |
+
s.add(pos("Shirt", "orange") == pos("Source", "backyard"))
|
130 |
+
|
131 |
+
# The producer with 12 oz jars is in the very first position.
|
132 |
+
s.add(pos("Size", "12 oz") == 1)
|
133 |
+
|
134 |
+
# Solve the puzzle
|
135 |
+
if s.check() == sat:
|
136 |
+
while s.check() == sat:
|
137 |
+
m = s.model()
|
138 |
+
rows = []
|
139 |
+
header = ["House"] + [cat_name for cat_name, _ in categories]
|
140 |
+
for position in range(1, NUM_POSITIONS + 1):
|
141 |
+
row = [str(position)]
|
142 |
+
for cat_idx, (cat_name, item_list) in enumerate(categories):
|
143 |
+
for item_idx, item_str in enumerate(item_list):
|
144 |
+
if m.evaluate(Vars[cat_idx][item_idx]).as_long() == position:
|
145 |
+
row.append(item_str)
|
146 |
+
break
|
147 |
+
rows.append(row)
|
148 |
+
result_dict = {"header": header, "rows": rows}
|
149 |
+
print(json.dumps(result_dict))
|
150 |
+
|
151 |
+
block = []
|
152 |
+
for cat_idx, (cat_name, item_list) in enumerate(categories):
|
153 |
+
for i in range(NUM_POSITIONS):
|
154 |
+
block.append(Vars[cat_idx][i] != m[Vars[cat_idx][i]])
|
155 |
+
s.add(Or(block))
|
156 |
+
|
157 |
+
if len(solutions) >= 10:
|
158 |
+
break
|
159 |
+
else:
|
160 |
+
print("NO_SOLUTION")
|
161 |
+
sys.stdout.flush() # Force immediate output
|
162 |
+
```
|
163 |
+
|
164 |
+
Based on this example, write a similar Z3-based Python code by changing only categories and constraints to represent the puzzle given below.
|
backend/app.py
CHANGED
@@ -3,12 +3,15 @@ from flask_cors import CORS
|
|
3 |
import os
|
4 |
|
5 |
from puzzle_dataset import get_puzzle_by_index
|
6 |
-
from solver import solve_puzzle
|
7 |
|
8 |
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
9 |
example_path = os.path.join(BASE_DIR, "Example.txt")
|
10 |
with open(example_path, "r", encoding="utf-8") as f:
|
11 |
DEFAULT_SYS_CONTENT = f.read()
|
|
|
|
|
|
|
12 |
sat_cnt_path = os.path.join(BASE_DIR, "Sat_cnt.txt")
|
13 |
with open(sat_cnt_path, "r", encoding="utf-8") as f:
|
14 |
SAT_CNT_CONTENT = f.read()
|
@@ -55,9 +58,25 @@ def solve():
|
|
55 |
result = solve_puzzle(puzzle_index, puzzle_text, expected_solution, sys_content, SAT_CNT_CONTENT)
|
56 |
return jsonify({"success": True, "result": result})
|
57 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
58 |
@app.route("/default_sys_content", methods=["GET"])
|
59 |
def get_default_sys_content():
|
60 |
return jsonify({"success": True, "sysContent": DEFAULT_SYS_CONTENT})
|
61 |
|
|
|
|
|
|
|
|
|
62 |
if __name__ == "__main__":
|
63 |
app.run(host="0.0.0.0", port=7860, debug=False)
|
|
|
3 |
import os
|
4 |
|
5 |
from puzzle_dataset import get_puzzle_by_index
|
6 |
+
from solver import solve_puzzle, solve_custom_puzzle
|
7 |
|
8 |
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
9 |
example_path = os.path.join(BASE_DIR, "Example.txt")
|
10 |
with open(example_path, "r", encoding="utf-8") as f:
|
11 |
DEFAULT_SYS_CONTENT = f.read()
|
12 |
+
example_multi_path = os.path.join(BASE_DIR, "Example_multi.txt")
|
13 |
+
with open(example_multi_path, "r", encoding="utf-8") as f:
|
14 |
+
DEFAULT_SYS_CONTENT_MULTI = f.read()
|
15 |
sat_cnt_path = os.path.join(BASE_DIR, "Sat_cnt.txt")
|
16 |
with open(sat_cnt_path, "r", encoding="utf-8") as f:
|
17 |
SAT_CNT_CONTENT = f.read()
|
|
|
58 |
result = solve_puzzle(puzzle_index, puzzle_text, expected_solution, sys_content, SAT_CNT_CONTENT)
|
59 |
return jsonify({"success": True, "result": result})
|
60 |
|
61 |
+
@app.route("/solve_custom", methods=["POST"])
|
62 |
+
def solve_custom():
|
63 |
+
data = request.get_json()
|
64 |
+
puzzle_text = data.get("puzzle")
|
65 |
+
sys_content = data.get("sys_content", DEFAULT_SYS_CONTENT_MULTI)
|
66 |
+
|
67 |
+
if not puzzle_text:
|
68 |
+
return jsonify({"success": False, "error": "Missing puzzle text"}), 400
|
69 |
+
|
70 |
+
result = solve_custom_puzzle(puzzle_text, sys_content, SAT_CNT_CONTENT)
|
71 |
+
return jsonify({"success": True, "result": result})
|
72 |
+
|
73 |
@app.route("/default_sys_content", methods=["GET"])
|
74 |
def get_default_sys_content():
|
75 |
return jsonify({"success": True, "sysContent": DEFAULT_SYS_CONTENT})
|
76 |
|
77 |
+
@app.route("/default_sys_content_multi", methods=["GET"])
|
78 |
+
def get_default_sys_content_multi():
|
79 |
+
return jsonify({"success": True, "sysContent": DEFAULT_SYS_CONTENT_MULTI})
|
80 |
+
|
81 |
if __name__ == "__main__":
|
82 |
app.run(host="0.0.0.0", port=7860, debug=False)
|
backend/solver.py
CHANGED
@@ -206,3 +206,85 @@ def solve_puzzle(index, puzzle, expected_solution, sys_content, sat_cnt_content)
|
|
206 |
"modelResponse": content,
|
207 |
"problematicConstraints": problematic_constraints
|
208 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
206 |
"modelResponse": content,
|
207 |
"problematicConstraints": problematic_constraints
|
208 |
}
|
209 |
+
|
210 |
+
def solve_custom_puzzle(puzzle, sys_content, sat_cnt_content):
|
211 |
+
"""
|
212 |
+
解决自定义谜题,返回所有可能的解
|
213 |
+
"""
|
214 |
+
load_dotenv()
|
215 |
+
client = OpenAI(
|
216 |
+
api_key=os.getenv("GEMINI_API_KEY"),
|
217 |
+
base_url="https://generativelanguage.googleapis.com/v1beta/openai/"
|
218 |
+
)
|
219 |
+
|
220 |
+
messages = [
|
221 |
+
{"role": "user", "content": sys_content},
|
222 |
+
{"role": "user", "content": puzzle},
|
223 |
+
]
|
224 |
+
|
225 |
+
attempts = 0
|
226 |
+
while attempts < 3:
|
227 |
+
messages.append({"role": "user", "content": "Make sure you stick to the given categories with strictly identical names otherwise there will be a critical error."})
|
228 |
+
attempts += 1
|
229 |
+
response = client.chat.completions.create(
|
230 |
+
model="gemini-2.0-flash",
|
231 |
+
messages=messages,
|
232 |
+
temperature=0.0,
|
233 |
+
stream=False
|
234 |
+
)
|
235 |
+
content = response.choices[0].message.content
|
236 |
+
messages.append(response.choices[0].message)
|
237 |
+
|
238 |
+
# 提取代码
|
239 |
+
code_blocks = re.findall(r"```(?:python)?(.*?)```", content, re.DOTALL)
|
240 |
+
if not code_blocks:
|
241 |
+
messages.append({"role": "user", "content": "Please write a complete Python code in your response. Try again."})
|
242 |
+
continue
|
243 |
+
|
244 |
+
code_to_run = code_blocks[-1].strip()
|
245 |
+
result = subprocess.run(
|
246 |
+
[sys.executable, "-c", code_to_run],
|
247 |
+
stdout=subprocess.PIPE,
|
248 |
+
stderr=subprocess.PIPE,
|
249 |
+
text=True
|
250 |
+
)
|
251 |
+
|
252 |
+
if result.stderr.strip():
|
253 |
+
messages.append({"role": "user", "content": f"Your code has errors: {result.stderr.strip()}. Please check your code and provide the complete code in the end."})
|
254 |
+
continue
|
255 |
+
|
256 |
+
output = result.stdout.strip()
|
257 |
+
try:
|
258 |
+
if output.startswith("NO_SOLUTION"):
|
259 |
+
return {
|
260 |
+
"no_solution": True,
|
261 |
+
"message": "No solution exists for this puzzle",
|
262 |
+
"generatedCode": code_to_run
|
263 |
+
}
|
264 |
+
else:
|
265 |
+
solutions = []
|
266 |
+
for line in output.split('\n'):
|
267 |
+
if line.strip():
|
268 |
+
try:
|
269 |
+
solution = json.loads(line)
|
270 |
+
solutions.append(solution)
|
271 |
+
except json.JSONDecodeError:
|
272 |
+
continue
|
273 |
+
|
274 |
+
if solutions:
|
275 |
+
return {
|
276 |
+
"solutions": solutions,
|
277 |
+
"generatedCode": code_to_run
|
278 |
+
}
|
279 |
+
else:
|
280 |
+
messages.append({"role": "user", "content": "Your output is not valid JSON. Please ensure your code prints solutions as JSON dictionaries."})
|
281 |
+
continue
|
282 |
+
except Exception as e:
|
283 |
+
messages.append({"role": "user", "content": f"Error processing output: {str(e)}. Please check your code format."})
|
284 |
+
continue
|
285 |
+
|
286 |
+
return {
|
287 |
+
"error": "Failed to solve puzzle after 3 attempts",
|
288 |
+
"generatedCode": code_to_run if 'code_to_run' in locals() else ""
|
289 |
+
}
|
290 |
+
|
frontend/src/App.css
CHANGED
@@ -431,6 +431,243 @@
|
|
431 |
backdrop-filter: blur(10px);
|
432 |
}
|
433 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
434 |
/* Responsive Design */
|
435 |
@media (max-width: 768px) {
|
436 |
.app-header h1 {
|
@@ -460,6 +697,33 @@
|
|
460 |
flex-direction: column;
|
461 |
gap: 1rem;
|
462 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
463 |
}
|
464 |
|
465 |
@media (max-width: 480px) {
|
@@ -475,4 +739,18 @@
|
|
475 |
padding: 0.5rem 1rem;
|
476 |
font-size: 0.9rem;
|
477 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
478 |
}
|
|
|
431 |
backdrop-filter: blur(10px);
|
432 |
}
|
433 |
|
434 |
+
/* Page Navigation */
|
435 |
+
.page-navigation {
|
436 |
+
display: flex;
|
437 |
+
gap: 1rem;
|
438 |
+
margin-top: 2rem;
|
439 |
+
justify-content: center;
|
440 |
+
}
|
441 |
+
|
442 |
+
.nav-tab {
|
443 |
+
padding: 0.75rem 2rem;
|
444 |
+
border: 2px solid rgba(255, 255, 255, 0.3);
|
445 |
+
border-radius: 25px;
|
446 |
+
background: rgba(255, 255, 255, 0.1);
|
447 |
+
color: white;
|
448 |
+
font-size: 1rem;
|
449 |
+
font-weight: 500;
|
450 |
+
cursor: pointer;
|
451 |
+
transition: all 0.3s ease;
|
452 |
+
backdrop-filter: blur(10px);
|
453 |
+
}
|
454 |
+
|
455 |
+
.nav-tab:hover {
|
456 |
+
background: rgba(255, 255, 255, 0.2);
|
457 |
+
border-color: rgba(255, 255, 255, 0.5);
|
458 |
+
}
|
459 |
+
|
460 |
+
.nav-tab.active {
|
461 |
+
background: white;
|
462 |
+
color: #667eea;
|
463 |
+
border-color: white;
|
464 |
+
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
|
465 |
+
}
|
466 |
+
|
467 |
+
/* Custom Puzzle Page */
|
468 |
+
.custom-puzzle-editor h3 {
|
469 |
+
margin-bottom: 0.5rem;
|
470 |
+
color: #333;
|
471 |
+
}
|
472 |
+
|
473 |
+
.puzzle-input-container {
|
474 |
+
margin-bottom: 2rem;
|
475 |
+
}
|
476 |
+
|
477 |
+
.custom-puzzle-textarea {
|
478 |
+
width: 100%;
|
479 |
+
min-height: 400px;
|
480 |
+
padding: 1rem;
|
481 |
+
border: 2px solid #e0e0e0;
|
482 |
+
border-radius: 8px;
|
483 |
+
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
484 |
+
font-size: 0.9rem;
|
485 |
+
line-height: 1.5;
|
486 |
+
resize: vertical;
|
487 |
+
transition: border-color 0.3s ease;
|
488 |
+
}
|
489 |
+
|
490 |
+
.custom-puzzle-textarea:focus {
|
491 |
+
outline: none;
|
492 |
+
border-color: #667eea;
|
493 |
+
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
|
494 |
+
}
|
495 |
+
|
496 |
+
.puzzle-examples {
|
497 |
+
background: #f8f9ff;
|
498 |
+
border-radius: 8px;
|
499 |
+
padding: 1.5rem;
|
500 |
+
border-left: 4px solid #667eea;
|
501 |
+
}
|
502 |
+
|
503 |
+
.puzzle-examples h4 {
|
504 |
+
margin-bottom: 1rem;
|
505 |
+
color: #333;
|
506 |
+
}
|
507 |
+
|
508 |
+
.puzzle-examples ul {
|
509 |
+
margin-left: 1.5rem;
|
510 |
+
color: #555;
|
511 |
+
line-height: 1.6;
|
512 |
+
}
|
513 |
+
|
514 |
+
.puzzle-examples li {
|
515 |
+
margin-bottom: 0.5rem;
|
516 |
+
}
|
517 |
+
|
518 |
+
/* Solution Navigator */
|
519 |
+
.solution-navigator {
|
520 |
+
background: #f8f9fa;
|
521 |
+
border-radius: 10px;
|
522 |
+
padding: 1.5rem;
|
523 |
+
margin-bottom: 2rem;
|
524 |
+
}
|
525 |
+
|
526 |
+
.solution-header {
|
527 |
+
display: flex;
|
528 |
+
justify-content: space-between;
|
529 |
+
align-items: center;
|
530 |
+
margin-bottom: 1.5rem;
|
531 |
+
}
|
532 |
+
|
533 |
+
.solution-header h3 {
|
534 |
+
margin: 0;
|
535 |
+
color: #333;
|
536 |
+
}
|
537 |
+
|
538 |
+
.solution-controls {
|
539 |
+
display: flex;
|
540 |
+
align-items: center;
|
541 |
+
gap: 1rem;
|
542 |
+
}
|
543 |
+
|
544 |
+
.btn-sm {
|
545 |
+
padding: 0.5rem 1rem;
|
546 |
+
font-size: 0.9rem;
|
547 |
+
}
|
548 |
+
|
549 |
+
.solution-pagination {
|
550 |
+
display: flex;
|
551 |
+
gap: 0.25rem;
|
552 |
+
}
|
553 |
+
|
554 |
+
.pagination-btn {
|
555 |
+
width: 35px;
|
556 |
+
height: 35px;
|
557 |
+
border: 1px solid #ddd;
|
558 |
+
border-radius: 6px;
|
559 |
+
background: white;
|
560 |
+
color: #666;
|
561 |
+
font-size: 0.9rem;
|
562 |
+
cursor: pointer;
|
563 |
+
transition: all 0.2s ease;
|
564 |
+
}
|
565 |
+
|
566 |
+
.pagination-btn:hover {
|
567 |
+
background: #f0f0f0;
|
568 |
+
}
|
569 |
+
|
570 |
+
.pagination-btn.active {
|
571 |
+
background: #667eea;
|
572 |
+
color: white;
|
573 |
+
border-color: #667eea;
|
574 |
+
}
|
575 |
+
|
576 |
+
.solution-display {
|
577 |
+
background: white;
|
578 |
+
border-radius: 8px;
|
579 |
+
padding: 1rem;
|
580 |
+
border: 1px solid #e0e0e0;
|
581 |
+
}
|
582 |
+
|
583 |
+
.solution-info {
|
584 |
+
margin-bottom: 1rem;
|
585 |
+
padding-bottom: 0.5rem;
|
586 |
+
border-bottom: 1px solid #eee;
|
587 |
+
}
|
588 |
+
|
589 |
+
.solution-counter {
|
590 |
+
color: #666;
|
591 |
+
font-weight: 500;
|
592 |
+
}
|
593 |
+
|
594 |
+
.solution-table-container {
|
595 |
+
overflow-x: auto;
|
596 |
+
}
|
597 |
+
|
598 |
+
.solution-table {
|
599 |
+
width: 100%;
|
600 |
+
border-collapse: collapse;
|
601 |
+
font-size: 0.9rem;
|
602 |
+
}
|
603 |
+
|
604 |
+
.solution-table th,
|
605 |
+
.solution-table td {
|
606 |
+
padding: 0.75rem;
|
607 |
+
text-align: left;
|
608 |
+
border: 1px solid #ddd;
|
609 |
+
}
|
610 |
+
|
611 |
+
.solution-table th {
|
612 |
+
background: #f8f9fa;
|
613 |
+
font-weight: 600;
|
614 |
+
color: #333;
|
615 |
+
}
|
616 |
+
|
617 |
+
.solution-table tr:nth-child(even) {
|
618 |
+
background: #f9f9f9;
|
619 |
+
}
|
620 |
+
|
621 |
+
.solution-table tr:hover {
|
622 |
+
background: #f0f4ff;
|
623 |
+
}
|
624 |
+
|
625 |
+
.solution-json {
|
626 |
+
background: #f8f9fa;
|
627 |
+
border-radius: 6px;
|
628 |
+
padding: 1rem;
|
629 |
+
overflow-x: auto;
|
630 |
+
}
|
631 |
+
|
632 |
+
.solution-json pre {
|
633 |
+
margin: 0;
|
634 |
+
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
635 |
+
font-size: 0.85rem;
|
636 |
+
line-height: 1.4;
|
637 |
+
}
|
638 |
+
|
639 |
+
/* Error Section */
|
640 |
+
.error-section {
|
641 |
+
margin-top: 2rem;
|
642 |
+
}
|
643 |
+
|
644 |
+
.error-section h3 {
|
645 |
+
margin-bottom: 1rem;
|
646 |
+
color: #dc3545;
|
647 |
+
}
|
648 |
+
|
649 |
+
.error-display {
|
650 |
+
background: #fff5f5;
|
651 |
+
border: 1px solid #fed7d7;
|
652 |
+
border-radius: 8px;
|
653 |
+
padding: 1rem;
|
654 |
+
max-height: 300px;
|
655 |
+
overflow-y: auto;
|
656 |
+
}
|
657 |
+
|
658 |
+
.error-display pre {
|
659 |
+
margin: 0;
|
660 |
+
color: #c53030;
|
661 |
+
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
662 |
+
font-size: 0.9rem;
|
663 |
+
line-height: 1.5;
|
664 |
+
white-space: pre-wrap;
|
665 |
+
}
|
666 |
+
|
667 |
+
.result-value.warning {
|
668 |
+
color: #f56500;
|
669 |
+
}
|
670 |
+
|
671 |
/* Responsive Design */
|
672 |
@media (max-width: 768px) {
|
673 |
.app-header h1 {
|
|
|
697 |
flex-direction: column;
|
698 |
gap: 1rem;
|
699 |
}
|
700 |
+
|
701 |
+
.page-navigation {
|
702 |
+
flex-direction: column;
|
703 |
+
align-items: center;
|
704 |
+
}
|
705 |
+
|
706 |
+
.nav-tab {
|
707 |
+
width: 200px;
|
708 |
+
text-align: center;
|
709 |
+
}
|
710 |
+
|
711 |
+
.solution-header {
|
712 |
+
flex-direction: column;
|
713 |
+
gap: 1rem;
|
714 |
+
align-items: stretch;
|
715 |
+
}
|
716 |
+
|
717 |
+
.solution-controls {
|
718 |
+
justify-content: center;
|
719 |
+
flex-wrap: wrap;
|
720 |
+
}
|
721 |
+
|
722 |
+
.solution-pagination {
|
723 |
+
max-width: 100%;
|
724 |
+
overflow-x: auto;
|
725 |
+
padding: 0.5rem 0;
|
726 |
+
}
|
727 |
}
|
728 |
|
729 |
@media (max-width: 480px) {
|
|
|
739 |
padding: 0.5rem 1rem;
|
740 |
font-size: 0.9rem;
|
741 |
}
|
742 |
+
|
743 |
+
.custom-puzzle-textarea {
|
744 |
+
min-height: 300px;
|
745 |
+
font-size: 0.8rem;
|
746 |
+
}
|
747 |
+
|
748 |
+
.solution-table {
|
749 |
+
font-size: 0.8rem;
|
750 |
+
}
|
751 |
+
|
752 |
+
.solution-table th,
|
753 |
+
.solution-table td {
|
754 |
+
padding: 0.5rem;
|
755 |
+
}
|
756 |
}
|
frontend/src/App.js
CHANGED
@@ -1,125 +1,19 @@
|
|
1 |
-
import React, { useState
|
2 |
import './App.css';
|
|
|
|
|
3 |
|
4 |
function App() {
|
5 |
-
|
6 |
-
|
7 |
-
const
|
8 |
-
|
9 |
-
|
10 |
-
|
11 |
-
|
12 |
-
|
13 |
-
|
14 |
-
|
15 |
-
const [executionSuccess, setExecutionSuccess] = useState(null);
|
16 |
-
const [attempts, setAttempts] = useState(0);
|
17 |
-
const [isSolving, setIsSolving] = useState(false);
|
18 |
-
const [problematicConstraints, setProblematicConstraints] = useState("");
|
19 |
-
|
20 |
-
// UI state
|
21 |
-
const [currentStep, setCurrentStep] = useState(1);
|
22 |
-
const [expandedSections, setExpandedSections] = useState({
|
23 |
-
puzzle: true,
|
24 |
-
sysContent: false,
|
25 |
-
result: false
|
26 |
-
});
|
27 |
-
|
28 |
-
// Frontend fetch sysContent in default
|
29 |
-
useEffect(() => {
|
30 |
-
fetch(`/default_sys_content`)
|
31 |
-
.then(res => res.json())
|
32 |
-
.then(data => {
|
33 |
-
if(data.success) {
|
34 |
-
setSysContent(data.sysContent);
|
35 |
-
}
|
36 |
-
})
|
37 |
-
.catch(e => console.error(e));
|
38 |
-
}, []);
|
39 |
-
|
40 |
-
// When puzzleIndex changing,auto get puzzle
|
41 |
-
useEffect(() => {
|
42 |
-
fetch(`/get_puzzle?index=${puzzleIndex}`)
|
43 |
-
.then(res => res.json())
|
44 |
-
.then(data => {
|
45 |
-
if(data.success) {
|
46 |
-
setPuzzleText(data.puzzle);
|
47 |
-
setExpectedSolution(data.expected_solution);
|
48 |
-
} else {
|
49 |
-
console.error("Failed to fetch puzzle", data.error);
|
50 |
-
setPuzzleText("");
|
51 |
-
setExpectedSolution(null);
|
52 |
-
}
|
53 |
-
})
|
54 |
-
.catch(e => console.error(e));
|
55 |
-
}, [puzzleIndex]);
|
56 |
-
|
57 |
-
const handleSolve = () => {
|
58 |
-
if(!puzzleText || !expectedSolution) {
|
59 |
-
alert("puzzle or expectedSolution incomplete");
|
60 |
-
return;
|
61 |
-
}
|
62 |
-
const payload = {
|
63 |
-
index: puzzleIndex,
|
64 |
-
puzzle: puzzleText,
|
65 |
-
expected_solution: expectedSolution,
|
66 |
-
sys_content: sysContent,
|
67 |
-
problematic_constraints: problematicConstraints
|
68 |
-
};
|
69 |
-
|
70 |
-
setIsSolving(true);
|
71 |
-
setCurrentStep(3);
|
72 |
-
setExpandedSections({...expandedSections, result: true});
|
73 |
-
|
74 |
-
fetch(`/solve`, {
|
75 |
-
method: "POST",
|
76 |
-
headers: { "Content-Type": "application/json" },
|
77 |
-
body: JSON.stringify(payload)
|
78 |
-
})
|
79 |
-
.then(res => res.json())
|
80 |
-
.then(data => {
|
81 |
-
if(!data.success) {
|
82 |
-
alert("Backend error: " + data.error);
|
83 |
-
return;
|
84 |
-
}
|
85 |
-
const result = data.result;
|
86 |
-
setGeneratedCode(result.generatedCode || "");
|
87 |
-
setExecutionSuccess(result.success);
|
88 |
-
setAttempts(result.attempts || 0);
|
89 |
-
setProblematicConstraints(result.problematicConstraints || "");
|
90 |
-
})
|
91 |
-
.catch(e => console.error(e))
|
92 |
-
.finally(() => {
|
93 |
-
setIsSolving(false);
|
94 |
-
});
|
95 |
-
};
|
96 |
-
|
97 |
-
const toggleSection = (section) => {
|
98 |
-
setExpandedSections({
|
99 |
-
...expandedSections,
|
100 |
-
[section]: !expandedSections[section]
|
101 |
-
});
|
102 |
-
};
|
103 |
-
|
104 |
-
const nextStep = () => {
|
105 |
-
if (currentStep < 3) {
|
106 |
-
setCurrentStep(currentStep + 1);
|
107 |
-
if (currentStep === 1) {
|
108 |
-
setExpandedSections({puzzle: false, sysContent: true, result: false});
|
109 |
-
} else if (currentStep === 2) {
|
110 |
-
setExpandedSections({puzzle: false, sysContent: false, result: true});
|
111 |
-
}
|
112 |
-
}
|
113 |
-
};
|
114 |
-
|
115 |
-
const prevStep = () => {
|
116 |
-
if (currentStep > 1) {
|
117 |
-
setCurrentStep(currentStep - 1);
|
118 |
-
if (currentStep === 2) {
|
119 |
-
setExpandedSections({puzzle: true, sysContent: false, result: false});
|
120 |
-
} else if (currentStep === 3) {
|
121 |
-
setExpandedSections({puzzle: false, sysContent: true, result: false});
|
122 |
-
}
|
123 |
}
|
124 |
};
|
125 |
|
@@ -128,181 +22,25 @@ function App() {
|
|
128 |
<header className="app-header">
|
129 |
<h1>🦓 Zebra Puzzle Solver</h1>
|
130 |
<p>Solve complex logic puzzles using AI-powered constraint satisfaction</p>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
131 |
</header>
|
132 |
|
133 |
-
{
|
134 |
-
<div className="progress-container">
|
135 |
-
<div className="progress-steps">
|
136 |
-
<div className={`step ${currentStep >= 1 ? 'active' : ''}`}>
|
137 |
-
<div className="step-number">1</div>
|
138 |
-
<div className="step-label">Select Puzzle</div>
|
139 |
-
</div>
|
140 |
-
<div className={`step ${currentStep >= 2 ? 'active' : ''}`}>
|
141 |
-
<div className="step-number">2</div>
|
142 |
-
<div className="step-label">Configure System</div>
|
143 |
-
</div>
|
144 |
-
<div className={`step ${currentStep >= 3 ? 'active' : ''}`}>
|
145 |
-
<div className="step-number">3</div>
|
146 |
-
<div className="step-label">Solve & Results</div>
|
147 |
-
</div>
|
148 |
-
</div>
|
149 |
-
</div>
|
150 |
-
|
151 |
-
<main className="main-content">
|
152 |
-
{/* Step 1: Puzzle Selection */}
|
153 |
-
<section className={`content-section ${expandedSections.puzzle ? 'expanded' : 'collapsed'}`}>
|
154 |
-
<div className="section-header" onClick={() => toggleSection('puzzle')}>
|
155 |
-
<h2>📋 Puzzle Selection</h2>
|
156 |
-
<span className="toggle-icon">{expandedSections.puzzle ? '▼' : '▶'}</span>
|
157 |
-
</div>
|
158 |
-
|
159 |
-
{expandedSections.puzzle && (
|
160 |
-
<div className="section-content">
|
161 |
-
<div className="puzzle-selector">
|
162 |
-
<label htmlFor="puzzle-index">Choose puzzle index (0 - 999):</label>
|
163 |
-
<div className="input-group">
|
164 |
-
<input
|
165 |
-
id="puzzle-index"
|
166 |
-
type="number"
|
167 |
-
value={puzzleIndex}
|
168 |
-
onChange={(e) => setPuzzleIndex(Number(e.target.value))}
|
169 |
-
min={0}
|
170 |
-
max={999}
|
171 |
-
className="number-input"
|
172 |
-
/>
|
173 |
-
<button
|
174 |
-
onClick={() => setPuzzleIndex(puzzleIndex)}
|
175 |
-
className="btn btn-secondary"
|
176 |
-
>
|
177 |
-
Load Puzzle
|
178 |
-
</button>
|
179 |
-
</div>
|
180 |
-
</div>
|
181 |
-
|
182 |
-
<div className="puzzle-display">
|
183 |
-
<div className="puzzle-text">
|
184 |
-
<h3>📄 Puzzle Text</h3>
|
185 |
-
<div className="text-display">
|
186 |
-
{puzzleText || "Loading puzzle..."}
|
187 |
-
</div>
|
188 |
-
</div>
|
189 |
-
|
190 |
-
<div className="expected-solution">
|
191 |
-
<h3>🎯 Expected Solution</h3>
|
192 |
-
<div className="json-display">
|
193 |
-
{expectedSolution ? (
|
194 |
-
<pre>{JSON.stringify(expectedSolution, null, 2)}</pre>
|
195 |
-
) : (
|
196 |
-
"Loading solution..."
|
197 |
-
)}
|
198 |
-
</div>
|
199 |
-
</div>
|
200 |
-
</div>
|
201 |
-
</div>
|
202 |
-
)}
|
203 |
-
</section>
|
204 |
-
|
205 |
-
{/* Step 2: System Configuration */}
|
206 |
-
<section className={`content-section ${expandedSections.sysContent ? 'expanded' : 'collapsed'}`}>
|
207 |
-
<div className="section-header" onClick={() => toggleSection('sysContent')}>
|
208 |
-
<h2>⚙️ System Configuration</h2>
|
209 |
-
<span className="toggle-icon">{expandedSections.sysContent ? '▼' : '▶'}</span>
|
210 |
-
</div>
|
211 |
-
|
212 |
-
{expandedSections.sysContent && (
|
213 |
-
<div className="section-content">
|
214 |
-
<div className="sys-content-editor">
|
215 |
-
<h3>📝 System Content</h3>
|
216 |
-
<p className="description">
|
217 |
-
Edit the system prompt that will guide the AI in solving the puzzle:
|
218 |
-
</p>
|
219 |
-
<textarea
|
220 |
-
value={sysContent}
|
221 |
-
onChange={(e) => setSysContent(e.target.value)}
|
222 |
-
className="sys-content-textarea"
|
223 |
-
placeholder="Enter system content..."
|
224 |
-
/>
|
225 |
-
</div>
|
226 |
-
</div>
|
227 |
-
)}
|
228 |
-
</section>
|
229 |
-
|
230 |
-
{/* Step 3: Solve & Results */}
|
231 |
-
<section className={`content-section ${expandedSections.result ? 'expanded' : 'collapsed'}`}>
|
232 |
-
<div className="section-header" onClick={() => toggleSection('result')}>
|
233 |
-
<h2>🚀 Solve & Results</h2>
|
234 |
-
<span className="toggle-icon">{expandedSections.result ? '▼' : '▶'}</span>
|
235 |
-
</div>
|
236 |
-
|
237 |
-
{expandedSections.result && (
|
238 |
-
<div className="section-content">
|
239 |
-
<div className="solve-section">
|
240 |
-
<button
|
241 |
-
onClick={handleSolve}
|
242 |
-
disabled={isSolving || !puzzleText || !expectedSolution}
|
243 |
-
className={`btn btn-primary solve-btn ${isSolving ? 'loading' : ''}`}
|
244 |
-
>
|
245 |
-
{isSolving ? '🔄 Solving...' : '🧠 Solve Puzzle with AI'}
|
246 |
-
</button>
|
247 |
-
</div>
|
248 |
-
|
249 |
-
<div className="results-section">
|
250 |
-
<div className="result-summary">
|
251 |
-
<div className="result-item">
|
252 |
-
<span className="result-label">Status:</span>
|
253 |
-
<span className={`result-value ${executionSuccess === true ? 'success' : executionSuccess === false ? 'error' : 'pending'}`}>
|
254 |
-
{executionSuccess === null ? "⏳ Pending" : executionSuccess ? "✅ Success" : "❌ Failed"}
|
255 |
-
</span>
|
256 |
-
</div>
|
257 |
-
<div className="result-item">
|
258 |
-
<span className="result-label">Attempts:</span>
|
259 |
-
<span className="result-value">{attempts}</span>
|
260 |
-
</div>
|
261 |
-
</div>
|
262 |
-
|
263 |
-
{problematicConstraints && (
|
264 |
-
<div className="issues-section">
|
265 |
-
<h3>⚠️ Issues & Analysis</h3>
|
266 |
-
<div className="issues-display">
|
267 |
-
<pre>{problematicConstraints}</pre>
|
268 |
-
</div>
|
269 |
-
</div>
|
270 |
-
)}
|
271 |
-
|
272 |
-
{generatedCode && (
|
273 |
-
<div className="code-section">
|
274 |
-
<h3>💻 Generated Code</h3>
|
275 |
-
<div className="code-display">
|
276 |
-
<pre><code>{generatedCode}</code></pre>
|
277 |
-
</div>
|
278 |
-
</div>
|
279 |
-
)}
|
280 |
-
</div>
|
281 |
-
</div>
|
282 |
-
)}
|
283 |
-
</section>
|
284 |
-
</main>
|
285 |
-
|
286 |
-
{/* Navigation */}
|
287 |
-
<nav className="navigation">
|
288 |
-
<button
|
289 |
-
onClick={prevStep}
|
290 |
-
disabled={currentStep <= 1}
|
291 |
-
className="btn btn-outline"
|
292 |
-
>
|
293 |
-
← Previous
|
294 |
-
</button>
|
295 |
-
<span className="step-indicator">
|
296 |
-
Step {currentStep} of 3
|
297 |
-
</span>
|
298 |
-
<button
|
299 |
-
onClick={nextStep}
|
300 |
-
disabled={currentStep >= 3}
|
301 |
-
className="btn btn-outline"
|
302 |
-
>
|
303 |
-
Next →
|
304 |
-
</button>
|
305 |
-
</nav>
|
306 |
</div>
|
307 |
);
|
308 |
}
|
|
|
1 |
+
import React, { useState } from 'react';
|
2 |
import './App.css';
|
3 |
+
import PredefinedPuzzlePage from './pages/PredefinedPuzzlePage';
|
4 |
+
import CustomPuzzlePage from './pages/CustomPuzzlePage';
|
5 |
|
6 |
function App() {
|
7 |
+
const [currentPage, setCurrentPage] = useState('predefined');
|
8 |
+
|
9 |
+
const renderPage = () => {
|
10 |
+
switch (currentPage) {
|
11 |
+
case 'predefined':
|
12 |
+
return <PredefinedPuzzlePage />;
|
13 |
+
case 'custom':
|
14 |
+
return <CustomPuzzlePage />;
|
15 |
+
default:
|
16 |
+
return <PredefinedPuzzlePage />;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
17 |
}
|
18 |
};
|
19 |
|
|
|
22 |
<header className="app-header">
|
23 |
<h1>🦓 Zebra Puzzle Solver</h1>
|
24 |
<p>Solve complex logic puzzles using AI-powered constraint satisfaction</p>
|
25 |
+
|
26 |
+
{/* Navigation Tabs */}
|
27 |
+
<nav className="page-navigation">
|
28 |
+
<button
|
29 |
+
className={`nav-tab ${currentPage === 'predefined' ? 'active' : ''}`}
|
30 |
+
onClick={() => setCurrentPage('predefined')}
|
31 |
+
>
|
32 |
+
📚 Predefined Puzzles
|
33 |
+
</button>
|
34 |
+
<button
|
35 |
+
className={`nav-tab ${currentPage === 'custom' ? 'active' : ''}`}
|
36 |
+
onClick={() => setCurrentPage('custom')}
|
37 |
+
>
|
38 |
+
✏️ Custom Puzzle
|
39 |
+
</button>
|
40 |
+
</nav>
|
41 |
</header>
|
42 |
|
43 |
+
{renderPage()}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
44 |
</div>
|
45 |
);
|
46 |
}
|
frontend/src/components/SolutionNavigator.js
ADDED
@@ -0,0 +1,102 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import React from 'react';
|
2 |
+
|
3 |
+
function SolutionNavigator({ solutions, currentIndex, onIndexChange }) {
|
4 |
+
if (!solutions || solutions.length === 0) {
|
5 |
+
return null;
|
6 |
+
}
|
7 |
+
|
8 |
+
const currentSolution = solutions[currentIndex];
|
9 |
+
|
10 |
+
const nextSolution = () => {
|
11 |
+
if (currentIndex < solutions.length - 1) {
|
12 |
+
onIndexChange(currentIndex + 1);
|
13 |
+
}
|
14 |
+
};
|
15 |
+
|
16 |
+
const prevSolution = () => {
|
17 |
+
if (currentIndex > 0) {
|
18 |
+
onIndexChange(currentIndex - 1);
|
19 |
+
}
|
20 |
+
};
|
21 |
+
|
22 |
+
const goToSolution = (index) => {
|
23 |
+
onIndexChange(index);
|
24 |
+
};
|
25 |
+
|
26 |
+
return (
|
27 |
+
<div className="solution-navigator">
|
28 |
+
<div className="solution-header">
|
29 |
+
<h3>🎯 Solutions ({solutions.length} found)</h3>
|
30 |
+
|
31 |
+
{solutions.length > 1 && (
|
32 |
+
<div className="solution-controls">
|
33 |
+
<button
|
34 |
+
onClick={prevSolution}
|
35 |
+
disabled={currentIndex <= 0}
|
36 |
+
className="btn btn-sm btn-outline"
|
37 |
+
>
|
38 |
+
← Previous
|
39 |
+
</button>
|
40 |
+
|
41 |
+
<div className="solution-pagination">
|
42 |
+
{solutions.map((_, index) => (
|
43 |
+
<button
|
44 |
+
key={index}
|
45 |
+
onClick={() => goToSolution(index)}
|
46 |
+
className={`pagination-btn ${index === currentIndex ? 'active' : ''}`}
|
47 |
+
>
|
48 |
+
{index + 1}
|
49 |
+
</button>
|
50 |
+
))}
|
51 |
+
</div>
|
52 |
+
|
53 |
+
<button
|
54 |
+
onClick={nextSolution}
|
55 |
+
disabled={currentIndex >= solutions.length - 1}
|
56 |
+
className="btn btn-sm btn-outline"
|
57 |
+
>
|
58 |
+
Next →
|
59 |
+
</button>
|
60 |
+
</div>
|
61 |
+
)}
|
62 |
+
</div>
|
63 |
+
|
64 |
+
<div className="solution-display">
|
65 |
+
<div className="solution-info">
|
66 |
+
<span className="solution-counter">
|
67 |
+
Solution {currentIndex + 1} of {solutions.length}
|
68 |
+
</span>
|
69 |
+
</div>
|
70 |
+
|
71 |
+
<div className="solution-table-container">
|
72 |
+
{currentSolution.header && currentSolution.rows ? (
|
73 |
+
<table className="solution-table">
|
74 |
+
<thead>
|
75 |
+
<tr>
|
76 |
+
{currentSolution.header.map((header, index) => (
|
77 |
+
<th key={index}>{header}</th>
|
78 |
+
))}
|
79 |
+
</tr>
|
80 |
+
</thead>
|
81 |
+
<tbody>
|
82 |
+
{currentSolution.rows.map((row, rowIndex) => (
|
83 |
+
<tr key={rowIndex}>
|
84 |
+
{row.map((cell, cellIndex) => (
|
85 |
+
<td key={cellIndex}>{cell}</td>
|
86 |
+
))}
|
87 |
+
</tr>
|
88 |
+
))}
|
89 |
+
</tbody>
|
90 |
+
</table>
|
91 |
+
) : (
|
92 |
+
<div className="solution-json">
|
93 |
+
<pre>{JSON.stringify(currentSolution, null, 2)}</pre>
|
94 |
+
</div>
|
95 |
+
)}
|
96 |
+
</div>
|
97 |
+
</div>
|
98 |
+
</div>
|
99 |
+
);
|
100 |
+
}
|
101 |
+
|
102 |
+
export default SolutionNavigator;
|
frontend/src/pages/CustomPuzzlePage.js
ADDED
@@ -0,0 +1,321 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import React, { useState, useEffect } from 'react';
|
2 |
+
import SolutionNavigator from '../components/SolutionNavigator';
|
3 |
+
|
4 |
+
function CustomPuzzlePage() {
|
5 |
+
// Custom puzzle input
|
6 |
+
const [customPuzzleText, setCustomPuzzleText] = useState("");
|
7 |
+
const [sysContent, setSysContent] = useState("");
|
8 |
+
|
9 |
+
// Results
|
10 |
+
const [solutions, setSolutions] = useState([]);
|
11 |
+
const [currentSolutionIndex, setCurrentSolutionIndex] = useState(0);
|
12 |
+
const [generatedCode, setGeneratedCode] = useState("");
|
13 |
+
const [isSolving, setIsSolving] = useState(false);
|
14 |
+
const [solutionStatus, setSolutionStatus] = useState(""); // "success", "no_solution", "error"
|
15 |
+
const [errorMessage, setErrorMessage] = useState("");
|
16 |
+
|
17 |
+
// UI state
|
18 |
+
const [currentStep, setCurrentStep] = useState(1);
|
19 |
+
const [expandedSections, setExpandedSections] = useState({
|
20 |
+
puzzle: true,
|
21 |
+
sysContent: false,
|
22 |
+
result: false
|
23 |
+
});
|
24 |
+
|
25 |
+
// Load default system content
|
26 |
+
useEffect(() => {
|
27 |
+
fetch(`/default_sys_content_multi`)
|
28 |
+
.then(res => res.json())
|
29 |
+
.then(data => {
|
30 |
+
if(data.success) {
|
31 |
+
setSysContent(data.sysContent);
|
32 |
+
}
|
33 |
+
})
|
34 |
+
.catch(e => console.error(e));
|
35 |
+
}, []);
|
36 |
+
|
37 |
+
const handleSolveCustom = () => {
|
38 |
+
if(!customPuzzleText.trim()) {
|
39 |
+
alert("Please enter a puzzle text");
|
40 |
+
return;
|
41 |
+
}
|
42 |
+
|
43 |
+
const payload = {
|
44 |
+
puzzle: customPuzzleText,
|
45 |
+
sys_content: sysContent,
|
46 |
+
custom: true
|
47 |
+
};
|
48 |
+
|
49 |
+
setIsSolving(true);
|
50 |
+
setCurrentStep(3);
|
51 |
+
setExpandedSections({puzzle: false, sysContent: false, result: true});
|
52 |
+
setSolutions([]);
|
53 |
+
setCurrentSolutionIndex(0);
|
54 |
+
setSolutionStatus("");
|
55 |
+
setErrorMessage("");
|
56 |
+
|
57 |
+
fetch(`/solve_custom`, {
|
58 |
+
method: "POST",
|
59 |
+
headers: { "Content-Type": "application/json" },
|
60 |
+
body: JSON.stringify(payload)
|
61 |
+
})
|
62 |
+
.then(res => res.json())
|
63 |
+
.then(data => {
|
64 |
+
if(!data.success) {
|
65 |
+
setErrorMessage("Backend error: " + data.error);
|
66 |
+
setSolutionStatus("error");
|
67 |
+
return;
|
68 |
+
}
|
69 |
+
|
70 |
+
const result = data.result;
|
71 |
+
setGeneratedCode(result.generatedCode || "");
|
72 |
+
|
73 |
+
if (result.solutions && result.solutions.length > 0) {
|
74 |
+
setSolutions(result.solutions);
|
75 |
+
setSolutionStatus("success");
|
76 |
+
setCurrentSolutionIndex(0);
|
77 |
+
} else if (result.no_solution) {
|
78 |
+
setSolutionStatus("no_solution");
|
79 |
+
setErrorMessage(result.message || "No solution found for this puzzle");
|
80 |
+
} else {
|
81 |
+
setSolutionStatus("error");
|
82 |
+
setErrorMessage(result.error || "Unknown error occurred");
|
83 |
+
}
|
84 |
+
})
|
85 |
+
.catch(e => {
|
86 |
+
console.error(e);
|
87 |
+
setErrorMessage("Network error: " + e.message);
|
88 |
+
setSolutionStatus("error");
|
89 |
+
})
|
90 |
+
.finally(() => {
|
91 |
+
setIsSolving(false);
|
92 |
+
});
|
93 |
+
};
|
94 |
+
|
95 |
+
const toggleSection = (section) => {
|
96 |
+
setExpandedSections({
|
97 |
+
...expandedSections,
|
98 |
+
[section]: !expandedSections[section]
|
99 |
+
});
|
100 |
+
};
|
101 |
+
|
102 |
+
const nextStep = () => {
|
103 |
+
if (currentStep < 3) {
|
104 |
+
setCurrentStep(currentStep + 1);
|
105 |
+
if (currentStep === 1) {
|
106 |
+
setExpandedSections({puzzle: false, sysContent: true, result: false});
|
107 |
+
} else if (currentStep === 2) {
|
108 |
+
setExpandedSections({puzzle: false, sysContent: false, result: true});
|
109 |
+
}
|
110 |
+
}
|
111 |
+
};
|
112 |
+
|
113 |
+
const prevStep = () => {
|
114 |
+
if (currentStep > 1) {
|
115 |
+
setCurrentStep(currentStep - 1);
|
116 |
+
if (currentStep === 2) {
|
117 |
+
setExpandedSections({puzzle: true, sysContent: false, result: false});
|
118 |
+
} else if (currentStep === 3) {
|
119 |
+
setExpandedSections({puzzle: false, sysContent: true, result: false});
|
120 |
+
}
|
121 |
+
}
|
122 |
+
};
|
123 |
+
|
124 |
+
return (
|
125 |
+
<>
|
126 |
+
{/* Progress Steps */}
|
127 |
+
<div className="progress-container">
|
128 |
+
<div className="progress-steps">
|
129 |
+
<div className={`step ${currentStep >= 1 ? 'active' : ''}`}>
|
130 |
+
<div className="step-number">1</div>
|
131 |
+
<div className="step-label">Create Puzzle</div>
|
132 |
+
</div>
|
133 |
+
<div className={`step ${currentStep >= 2 ? 'active' : ''}`}>
|
134 |
+
<div className="step-number">2</div>
|
135 |
+
<div className="step-label">Configure System</div>
|
136 |
+
</div>
|
137 |
+
<div className={`step ${currentStep >= 3 ? 'active' : ''}`}>
|
138 |
+
<div className="step-number">3</div>
|
139 |
+
<div className="step-label">Solve & Results</div>
|
140 |
+
</div>
|
141 |
+
</div>
|
142 |
+
</div>
|
143 |
+
|
144 |
+
<main className="main-content">
|
145 |
+
{/* Step 1: Custom Puzzle Input */}
|
146 |
+
<section className={`content-section ${expandedSections.puzzle ? 'expanded' : 'collapsed'}`}>
|
147 |
+
<div className="section-header" onClick={() => toggleSection('puzzle')}>
|
148 |
+
<h2>✏️ Custom Puzzle Creation</h2>
|
149 |
+
<span className="toggle-icon">{expandedSections.puzzle ? '▼' : '▶'}</span>
|
150 |
+
</div>
|
151 |
+
|
152 |
+
{expandedSections.puzzle && (
|
153 |
+
<div className="section-content">
|
154 |
+
<div className="custom-puzzle-editor">
|
155 |
+
<h3>📝 Puzzle Description</h3>
|
156 |
+
<p className="description">
|
157 |
+
Enter your custom zebra puzzle. Include categories, items, and constraints:
|
158 |
+
</p>
|
159 |
+
<div className="puzzle-input-container">
|
160 |
+
<textarea
|
161 |
+
value={customPuzzleText}
|
162 |
+
onChange={(e) => setCustomPuzzleText(e.target.value)}
|
163 |
+
className="custom-puzzle-textarea"
|
164 |
+
placeholder="Example:
|
165 |
+
There are 5 houses in a row.
|
166 |
+
|
167 |
+
Categories:
|
168 |
+
- Color: red, blue, green, yellow, white
|
169 |
+
- Name: Alice, Bob, Carol, Dave, Eve
|
170 |
+
- Pet: cat, dog, fish, bird, rabbit
|
171 |
+
- Drink: tea, coffee, milk, juice, water
|
172 |
+
- Sport: tennis, soccer, golf, swimming, running
|
173 |
+
|
174 |
+
Constraints:
|
175 |
+
1. The person in the red house owns a cat.
|
176 |
+
2. Alice drinks tea.
|
177 |
+
3. The green house is to the left of the white house.
|
178 |
+
..."
|
179 |
+
/>
|
180 |
+
</div>
|
181 |
+
|
182 |
+
<div className="puzzle-examples">
|
183 |
+
<h4>💡 Tips for Creating Puzzles:</h4>
|
184 |
+
<ul>
|
185 |
+
<li>Clearly define all categories and their possible values</li>
|
186 |
+
<li>Number your constraints for clarity</li>
|
187 |
+
<li>Use spatial relationships like "to the left of", "next to", "between"</li>
|
188 |
+
<li>Include direct assignments like "Alice lives in the red house"</li>
|
189 |
+
<li>It will be better to have your expected results</li>
|
190 |
+
</ul>
|
191 |
+
</div>
|
192 |
+
</div>
|
193 |
+
</div>
|
194 |
+
)}
|
195 |
+
</section>
|
196 |
+
|
197 |
+
{/* Step 2: System Configuration */}
|
198 |
+
<section className={`content-section ${expandedSections.sysContent ? 'expanded' : 'collapsed'}`}>
|
199 |
+
<div className="section-header" onClick={() => toggleSection('sysContent')}>
|
200 |
+
<h2>⚙️ System Configuration</h2>
|
201 |
+
<span className="toggle-icon">{expandedSections.sysContent ? '▼' : '▶'}</span>
|
202 |
+
</div>
|
203 |
+
|
204 |
+
{expandedSections.sysContent && (
|
205 |
+
<div className="section-content">
|
206 |
+
<div className="sys-content-editor">
|
207 |
+
<h3>📝 System Content</h3>
|
208 |
+
<p className="description">
|
209 |
+
Edit the system prompt that will guide the AI in solving your custom puzzle:
|
210 |
+
</p>
|
211 |
+
<textarea
|
212 |
+
value={sysContent}
|
213 |
+
onChange={(e) => setSysContent(e.target.value)}
|
214 |
+
className="sys-content-textarea"
|
215 |
+
placeholder="Enter system content..."
|
216 |
+
/>
|
217 |
+
</div>
|
218 |
+
</div>
|
219 |
+
)}
|
220 |
+
</section>
|
221 |
+
|
222 |
+
{/* Step 3: Solve & Results */}
|
223 |
+
<section className={`content-section ${expandedSections.result ? 'expanded' : 'collapsed'}`}>
|
224 |
+
<div className="section-header" onClick={() => toggleSection('result')}>
|
225 |
+
<h2>🚀 Solve & Results</h2>
|
226 |
+
<span className="toggle-icon">{expandedSections.result ? '▼' : '▶'}</span>
|
227 |
+
</div>
|
228 |
+
|
229 |
+
{expandedSections.result && (
|
230 |
+
<div className="section-content">
|
231 |
+
<div className="solve-section">
|
232 |
+
<button
|
233 |
+
onClick={handleSolveCustom}
|
234 |
+
disabled={isSolving || !customPuzzleText.trim()}
|
235 |
+
className={`btn btn-primary solve-btn ${isSolving ? 'loading' : ''}`}
|
236 |
+
>
|
237 |
+
{isSolving ? '🔄 Solving Custom Puzzle...' : '🧠 Solve Custom Puzzle'}
|
238 |
+
</button>
|
239 |
+
</div>
|
240 |
+
|
241 |
+
<div className="results-section">
|
242 |
+
<div className="result-summary">
|
243 |
+
<div className="result-item">
|
244 |
+
<span className="result-label">Status:</span>
|
245 |
+
<span className={`result-value ${
|
246 |
+
solutionStatus === 'success' ? 'success' :
|
247 |
+
solutionStatus === 'no_solution' ? 'warning' :
|
248 |
+
solutionStatus === 'error' ? 'error' : 'pending'
|
249 |
+
}`}>
|
250 |
+
{solutionStatus === 'success' ? "✅ Solved" :
|
251 |
+
solutionStatus === 'no_solution' ? "⚠️ No Solution" :
|
252 |
+
solutionStatus === 'error' ? "❌ Error" : "⏳ Pending"}
|
253 |
+
</span>
|
254 |
+
</div>
|
255 |
+
{solutions.length > 0 && (
|
256 |
+
<div className="result-item">
|
257 |
+
<span className="result-label">Solutions Found:</span>
|
258 |
+
<span className="result-value">{solutions.length}</span>
|
259 |
+
</div>
|
260 |
+
)}
|
261 |
+
</div>
|
262 |
+
|
263 |
+
{/* Solution Navigator */}
|
264 |
+
{solutions.length > 0 && (
|
265 |
+
<SolutionNavigator
|
266 |
+
solutions={solutions}
|
267 |
+
currentIndex={currentSolutionIndex}
|
268 |
+
onIndexChange={setCurrentSolutionIndex}
|
269 |
+
/>
|
270 |
+
)}
|
271 |
+
|
272 |
+
{/* Error Message */}
|
273 |
+
{(solutionStatus === 'error' || solutionStatus === 'no_solution') && errorMessage && (
|
274 |
+
<div className="error-section">
|
275 |
+
<h3>⚠️ {solutionStatus === 'no_solution' ? 'No Solution Found' : 'Error'}</h3>
|
276 |
+
<div className="error-display">
|
277 |
+
<pre>{errorMessage}</pre>
|
278 |
+
</div>
|
279 |
+
</div>
|
280 |
+
)}
|
281 |
+
|
282 |
+
{/* Generated Code */}
|
283 |
+
{generatedCode && (
|
284 |
+
<div className="code-section">
|
285 |
+
<h3>💻 Generated Code</h3>
|
286 |
+
<div className="code-display">
|
287 |
+
<pre><code>{generatedCode}</code></pre>
|
288 |
+
</div>
|
289 |
+
</div>
|
290 |
+
)}
|
291 |
+
</div>
|
292 |
+
</div>
|
293 |
+
)}
|
294 |
+
</section>
|
295 |
+
</main>
|
296 |
+
|
297 |
+
{/* Navigation */}
|
298 |
+
<nav className="navigation">
|
299 |
+
<button
|
300 |
+
onClick={prevStep}
|
301 |
+
disabled={currentStep <= 1}
|
302 |
+
className="btn btn-outline"
|
303 |
+
>
|
304 |
+
← Previous
|
305 |
+
</button>
|
306 |
+
<span className="step-indicator">
|
307 |
+
Step {currentStep} of 3
|
308 |
+
</span>
|
309 |
+
<button
|
310 |
+
onClick={nextStep}
|
311 |
+
disabled={currentStep >= 3}
|
312 |
+
className="btn btn-outline"
|
313 |
+
>
|
314 |
+
Next →
|
315 |
+
</button>
|
316 |
+
</nav>
|
317 |
+
</>
|
318 |
+
);
|
319 |
+
}
|
320 |
+
|
321 |
+
export default CustomPuzzlePage;
|
frontend/src/pages/PredefinedPuzzlePage.js
ADDED
@@ -0,0 +1,304 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import React, { useState, useEffect } from 'react';
|
2 |
+
|
3 |
+
function PredefinedPuzzlePage() {
|
4 |
+
// For puzzle index and puzzle data
|
5 |
+
const [puzzleIndex, setPuzzleIndex] = useState(0);
|
6 |
+
const [puzzleText, setPuzzleText] = useState("");
|
7 |
+
const [expectedSolution, setExpectedSolution] = useState(null);
|
8 |
+
|
9 |
+
// sysContent can be editted, default using Example.txt
|
10 |
+
const [sysContent, setSysContent] = useState("");
|
11 |
+
|
12 |
+
// Interaction results
|
13 |
+
const [generatedCode, setGeneratedCode] = useState("");
|
14 |
+
const [executionSuccess, setExecutionSuccess] = useState(null);
|
15 |
+
const [attempts, setAttempts] = useState(0);
|
16 |
+
const [isSolving, setIsSolving] = useState(false);
|
17 |
+
const [problematicConstraints, setProblematicConstraints] = useState("");
|
18 |
+
|
19 |
+
// UI state
|
20 |
+
const [currentStep, setCurrentStep] = useState(1);
|
21 |
+
const [expandedSections, setExpandedSections] = useState({
|
22 |
+
puzzle: true,
|
23 |
+
sysContent: false,
|
24 |
+
result: false
|
25 |
+
});
|
26 |
+
|
27 |
+
// Frontend fetch sysContent in default
|
28 |
+
useEffect(() => {
|
29 |
+
fetch(`/default_sys_content`)
|
30 |
+
.then(res => res.json())
|
31 |
+
.then(data => {
|
32 |
+
if(data.success) {
|
33 |
+
setSysContent(data.sysContent);
|
34 |
+
}
|
35 |
+
})
|
36 |
+
.catch(e => console.error(e));
|
37 |
+
}, []);
|
38 |
+
|
39 |
+
// When puzzleIndex changing,auto get puzzle
|
40 |
+
useEffect(() => {
|
41 |
+
fetch(`/get_puzzle?index=${puzzleIndex}`)
|
42 |
+
.then(res => res.json())
|
43 |
+
.then(data => {
|
44 |
+
if(data.success) {
|
45 |
+
setPuzzleText(data.puzzle);
|
46 |
+
setExpectedSolution(data.expected_solution);
|
47 |
+
} else {
|
48 |
+
console.error("Failed to fetch puzzle", data.error);
|
49 |
+
setPuzzleText("");
|
50 |
+
setExpectedSolution(null);
|
51 |
+
}
|
52 |
+
})
|
53 |
+
.catch(e => console.error(e));
|
54 |
+
}, [puzzleIndex]);
|
55 |
+
|
56 |
+
const handleSolve = () => {
|
57 |
+
if(!puzzleText || !expectedSolution) {
|
58 |
+
alert("puzzle or expectedSolution incomplete");
|
59 |
+
return;
|
60 |
+
}
|
61 |
+
const payload = {
|
62 |
+
index: puzzleIndex,
|
63 |
+
puzzle: puzzleText,
|
64 |
+
expected_solution: expectedSolution,
|
65 |
+
sys_content: sysContent,
|
66 |
+
problematic_constraints: problematicConstraints
|
67 |
+
};
|
68 |
+
|
69 |
+
setIsSolving(true);
|
70 |
+
setCurrentStep(3);
|
71 |
+
setExpandedSections({...expandedSections, result: true});
|
72 |
+
|
73 |
+
fetch(`/solve`, {
|
74 |
+
method: "POST",
|
75 |
+
headers: { "Content-Type": "application/json" },
|
76 |
+
body: JSON.stringify(payload)
|
77 |
+
})
|
78 |
+
.then(res => res.json())
|
79 |
+
.then(data => {
|
80 |
+
if(!data.success) {
|
81 |
+
alert("Backend error: " + data.error);
|
82 |
+
return;
|
83 |
+
}
|
84 |
+
const result = data.result;
|
85 |
+
setGeneratedCode(result.generatedCode || "");
|
86 |
+
setExecutionSuccess(result.success);
|
87 |
+
setAttempts(result.attempts || 0);
|
88 |
+
setProblematicConstraints(result.problematicConstraints || "");
|
89 |
+
})
|
90 |
+
.catch(e => console.error(e))
|
91 |
+
.finally(() => {
|
92 |
+
setIsSolving(false);
|
93 |
+
});
|
94 |
+
};
|
95 |
+
|
96 |
+
const toggleSection = (section) => {
|
97 |
+
setExpandedSections({
|
98 |
+
...expandedSections,
|
99 |
+
[section]: !expandedSections[section]
|
100 |
+
});
|
101 |
+
};
|
102 |
+
|
103 |
+
const nextStep = () => {
|
104 |
+
if (currentStep < 3) {
|
105 |
+
setCurrentStep(currentStep + 1);
|
106 |
+
if (currentStep === 1) {
|
107 |
+
setExpandedSections({puzzle: false, sysContent: true, result: false});
|
108 |
+
} else if (currentStep === 2) {
|
109 |
+
setExpandedSections({puzzle: false, sysContent: false, result: true});
|
110 |
+
}
|
111 |
+
}
|
112 |
+
};
|
113 |
+
|
114 |
+
const prevStep = () => {
|
115 |
+
if (currentStep > 1) {
|
116 |
+
setCurrentStep(currentStep - 1);
|
117 |
+
if (currentStep === 2) {
|
118 |
+
setExpandedSections({puzzle: true, sysContent: false, result: false});
|
119 |
+
} else if (currentStep === 3) {
|
120 |
+
setExpandedSections({puzzle: false, sysContent: true, result: false});
|
121 |
+
}
|
122 |
+
}
|
123 |
+
};
|
124 |
+
|
125 |
+
return (
|
126 |
+
<>
|
127 |
+
{/* Progress Steps */}
|
128 |
+
<div className="progress-container">
|
129 |
+
<div className="progress-steps">
|
130 |
+
<div className={`step ${currentStep >= 1 ? 'active' : ''}`}>
|
131 |
+
<div className="step-number">1</div>
|
132 |
+
<div className="step-label">Select Puzzle</div>
|
133 |
+
</div>
|
134 |
+
<div className={`step ${currentStep >= 2 ? 'active' : ''}`}>
|
135 |
+
<div className="step-number">2</div>
|
136 |
+
<div className="step-label">Configure System</div>
|
137 |
+
</div>
|
138 |
+
<div className={`step ${currentStep >= 3 ? 'active' : ''}`}>
|
139 |
+
<div className="step-number">3</div>
|
140 |
+
<div className="step-label">Solve & Results</div>
|
141 |
+
</div>
|
142 |
+
</div>
|
143 |
+
</div>
|
144 |
+
|
145 |
+
<main className="main-content">
|
146 |
+
{/* Step 1: Puzzle Selection */}
|
147 |
+
<section className={`content-section ${expandedSections.puzzle ? 'expanded' : 'collapsed'}`}>
|
148 |
+
<div className="section-header" onClick={() => toggleSection('puzzle')}>
|
149 |
+
<h2>📋 Puzzle Selection</h2>
|
150 |
+
<span className="toggle-icon">{expandedSections.puzzle ? '▼' : '▶'}</span>
|
151 |
+
</div>
|
152 |
+
|
153 |
+
{expandedSections.puzzle && (
|
154 |
+
<div className="section-content">
|
155 |
+
<div className="puzzle-selector">
|
156 |
+
<label htmlFor="puzzle-index">Choose puzzle index (0 - 999):</label>
|
157 |
+
<div className="input-group">
|
158 |
+
<input
|
159 |
+
id="puzzle-index"
|
160 |
+
type="number"
|
161 |
+
value={puzzleIndex}
|
162 |
+
onChange={(e) => setPuzzleIndex(Number(e.target.value))}
|
163 |
+
min={0}
|
164 |
+
max={999}
|
165 |
+
className="number-input"
|
166 |
+
/>
|
167 |
+
<button
|
168 |
+
onClick={() => setPuzzleIndex(puzzleIndex)}
|
169 |
+
className="btn btn-secondary"
|
170 |
+
>
|
171 |
+
Load Puzzle
|
172 |
+
</button>
|
173 |
+
</div>
|
174 |
+
</div>
|
175 |
+
|
176 |
+
<div className="puzzle-display">
|
177 |
+
<div className="puzzle-text">
|
178 |
+
<h3>📄 Puzzle Text</h3>
|
179 |
+
<div className="text-display">
|
180 |
+
{puzzleText || "Loading puzzle..."}
|
181 |
+
</div>
|
182 |
+
</div>
|
183 |
+
|
184 |
+
<div className="expected-solution">
|
185 |
+
<h3>🎯 Expected Solution</h3>
|
186 |
+
<div className="json-display">
|
187 |
+
{expectedSolution ? (
|
188 |
+
<pre>{JSON.stringify(expectedSolution, null, 2)}</pre>
|
189 |
+
) : (
|
190 |
+
"Loading solution..."
|
191 |
+
)}
|
192 |
+
</div>
|
193 |
+
</div>
|
194 |
+
</div>
|
195 |
+
</div>
|
196 |
+
)}
|
197 |
+
</section>
|
198 |
+
|
199 |
+
{/* Step 2: System Configuration */}
|
200 |
+
<section className={`content-section ${expandedSections.sysContent ? 'expanded' : 'collapsed'}`}>
|
201 |
+
<div className="section-header" onClick={() => toggleSection('sysContent')}>
|
202 |
+
<h2>⚙️ System Configuration</h2>
|
203 |
+
<span className="toggle-icon">{expandedSections.sysContent ? '▼' : '▶'}</span>
|
204 |
+
</div>
|
205 |
+
|
206 |
+
{expandedSections.sysContent && (
|
207 |
+
<div className="section-content">
|
208 |
+
<div className="sys-content-editor">
|
209 |
+
<h3>📝 System Content</h3>
|
210 |
+
<p className="description">
|
211 |
+
Edit the system prompt that will guide the AI in solving the puzzle:
|
212 |
+
</p>
|
213 |
+
<textarea
|
214 |
+
value={sysContent}
|
215 |
+
onChange={(e) => setSysContent(e.target.value)}
|
216 |
+
className="sys-content-textarea"
|
217 |
+
placeholder="Enter system content..."
|
218 |
+
/>
|
219 |
+
</div>
|
220 |
+
</div>
|
221 |
+
)}
|
222 |
+
</section>
|
223 |
+
|
224 |
+
{/* Step 3: Solve & Results */}
|
225 |
+
<section className={`content-section ${expandedSections.result ? 'expanded' : 'collapsed'}`}>
|
226 |
+
<div className="section-header" onClick={() => toggleSection('result')}>
|
227 |
+
<h2>🚀 Solve & Results</h2>
|
228 |
+
<span className="toggle-icon">{expandedSections.result ? '▼' : '▶'}</span>
|
229 |
+
</div>
|
230 |
+
|
231 |
+
{expandedSections.result && (
|
232 |
+
<div className="section-content">
|
233 |
+
<div className="solve-section">
|
234 |
+
<button
|
235 |
+
onClick={handleSolve}
|
236 |
+
disabled={isSolving || !puzzleText || !expectedSolution}
|
237 |
+
className={`btn btn-primary solve-btn ${isSolving ? 'loading' : ''}`}
|
238 |
+
>
|
239 |
+
{isSolving ? '🔄 Solving...' : '🧠 Solve Puzzle with AI'}
|
240 |
+
</button>
|
241 |
+
</div>
|
242 |
+
|
243 |
+
<div className="results-section">
|
244 |
+
<div className="result-summary">
|
245 |
+
<div className="result-item">
|
246 |
+
<span className="result-label">Status:</span>
|
247 |
+
<span className={`result-value ${executionSuccess === true ? 'success' : executionSuccess === false ? 'error' : 'pending'}`}>
|
248 |
+
{executionSuccess === null ? "⏳ Pending" : executionSuccess ? "✅ Success" : "❌ Failed"}
|
249 |
+
</span>
|
250 |
+
</div>
|
251 |
+
<div className="result-item">
|
252 |
+
<span className="result-label">Attempts:</span>
|
253 |
+
<span className="result-value">{attempts}</span>
|
254 |
+
</div>
|
255 |
+
</div>
|
256 |
+
|
257 |
+
{problematicConstraints && (
|
258 |
+
<div className="issues-section">
|
259 |
+
<h3>⚠️ Issues & Analysis</h3>
|
260 |
+
<div className="issues-display">
|
261 |
+
<pre>{problematicConstraints}</pre>
|
262 |
+
</div>
|
263 |
+
</div>
|
264 |
+
)}
|
265 |
+
|
266 |
+
{generatedCode && (
|
267 |
+
<div className="code-section">
|
268 |
+
<h3>💻 Generated Code</h3>
|
269 |
+
<div className="code-display">
|
270 |
+
<pre><code>{generatedCode}</code></pre>
|
271 |
+
</div>
|
272 |
+
</div>
|
273 |
+
)}
|
274 |
+
</div>
|
275 |
+
</div>
|
276 |
+
)}
|
277 |
+
</section>
|
278 |
+
</main>
|
279 |
+
|
280 |
+
{/* Navigation */}
|
281 |
+
<nav className="navigation">
|
282 |
+
<button
|
283 |
+
onClick={prevStep}
|
284 |
+
disabled={currentStep <= 1}
|
285 |
+
className="btn btn-outline"
|
286 |
+
>
|
287 |
+
← Previous
|
288 |
+
</button>
|
289 |
+
<span className="step-indicator">
|
290 |
+
Step {currentStep} of 3
|
291 |
+
</span>
|
292 |
+
<button
|
293 |
+
onClick={nextStep}
|
294 |
+
disabled={currentStep >= 3}
|
295 |
+
className="btn btn-outline"
|
296 |
+
>
|
297 |
+
Next →
|
298 |
+
</button>
|
299 |
+
</nav>
|
300 |
+
</>
|
301 |
+
);
|
302 |
+
}
|
303 |
+
|
304 |
+
export default PredefinedPuzzlePage;
|