blackopsrepl commited on
Commit
177c40c
·
verified ·
1 Parent(s): f1cee81

Upload 33 files

Browse files
Files changed (33) hide show
  1. .gitignore +44 -0
  2. Dockerfile +33 -0
  3. LICENSE +201 -0
  4. README_HUGGINGFACE.md +47 -0
  5. logging.conf +30 -0
  6. pyproject.toml +24 -0
  7. src/my_quickstart/__init__.py +21 -0
  8. src/my_quickstart/__pycache__/__init__.cpython-312.pyc +0 -0
  9. src/my_quickstart/__pycache__/constraints.cpython-312.pyc +0 -0
  10. src/my_quickstart/__pycache__/demo_data.cpython-312.pyc +0 -0
  11. src/my_quickstart/__pycache__/domain.cpython-312.pyc +0 -0
  12. src/my_quickstart/__pycache__/json_serialization.cpython-312.pyc +0 -0
  13. src/my_quickstart/__pycache__/rest_api.cpython-312.pyc +0 -0
  14. src/my_quickstart/__pycache__/solver.cpython-312.pyc +0 -0
  15. src/my_quickstart/constraints.py +239 -0
  16. src/my_quickstart/demo_data.py +84 -0
  17. src/my_quickstart/domain.py +141 -0
  18. src/my_quickstart/json_serialization.py +42 -0
  19. src/my_quickstart/rest_api.py +285 -0
  20. src/my_quickstart/solver.py +40 -0
  21. static/app.css +728 -0
  22. static/app.js +1836 -0
  23. static/index.html +856 -0
  24. static/webjars/solverforge/css/solverforge-webui.css +122 -0
  25. static/webjars/solverforge/img/solverforge-favicon.svg +65 -0
  26. static/webjars/solverforge/img/solverforge-horizontal-white.svg +66 -0
  27. static/webjars/solverforge/img/solverforge-horizontal.svg +65 -0
  28. static/webjars/solverforge/img/solverforge-logo-stacked.svg +73 -0
  29. static/webjars/solverforge/js/solverforge-webui.js +333 -0
  30. tests/__pycache__/test_constraints.cpython-313-pytest-9.0.2.pyc +0 -0
  31. tests/__pycache__/test_rest_api.cpython-313-pytest-9.0.2.pyc +0 -0
  32. tests/test_constraints.py +140 -0
  33. tests/test_rest_api.py +79 -0
.gitignore ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.so
6
+ .Python
7
+ build/
8
+ develop-eggs/
9
+ dist/
10
+ downloads/
11
+ eggs/
12
+ .eggs/
13
+ lib/
14
+ lib64/
15
+ parts/
16
+ sdist/
17
+ var/
18
+ wheels/
19
+ *.egg-info/
20
+ .installed.cfg
21
+ *.egg
22
+
23
+ # Virtual environments
24
+ .venv/
25
+ venv/
26
+ ENV/
27
+
28
+ # IDE
29
+ .idea/
30
+ .vscode/
31
+ *.swp
32
+ *.swo
33
+
34
+ # Testing
35
+ .pytest_cache/
36
+ .coverage
37
+ htmlcov/
38
+
39
+ # OS
40
+ .DS_Store
41
+ Thumbs.db
42
+
43
+ # Logs
44
+ *.log
Dockerfile ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SolverForge Quickstart Dockerfile
2
+ # Compatible with HuggingFace Spaces (Docker SDK)
3
+
4
+ FROM python:3.12
5
+
6
+ # Install JDK 21 (required for solverforge-legacy)
7
+ RUN apt-get update && \
8
+ apt-get install -y wget gnupg2 && \
9
+ wget -O- https://packages.adoptium.net/artifactory/api/gpg/key/public | gpg --dearmor > /usr/share/keyrings/adoptium-archive-keyring.gpg && \
10
+ echo "deb [signed-by=/usr/share/keyrings/adoptium-archive-keyring.gpg] https://packages.adoptium.net/artifactory/deb bookworm main" > /etc/apt/sources.list.d/adoptium.list && \
11
+ apt-get update && \
12
+ apt-get install -y temurin-21-jdk && \
13
+ apt-get clean && \
14
+ rm -rf /var/lib/apt/lists/*
15
+
16
+ # Set working directory
17
+ WORKDIR /app
18
+
19
+ # Copy application files
20
+ COPY . .
21
+
22
+ # Install the application
23
+ RUN pip install --no-cache-dir -e .
24
+
25
+ # HuggingFace Spaces uses port 7860 by default
26
+ # Our app uses 8080 internally, we'll configure uvicorn to use 7860
27
+ ENV PORT=7860
28
+
29
+ # Expose port
30
+ EXPOSE 7860
31
+
32
+ # Run the application (HuggingFace compatible)
33
+ CMD ["python", "-m", "uvicorn", "my_quickstart:app", "--host", "0.0.0.0", "--port", "7860"]
LICENSE ADDED
@@ -0,0 +1,201 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "[]"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright [yyyy] [name of copyright owner]
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.
README_HUGGINGFACE.md ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: SolverForge Quickstart
3
+ emoji: "+"
4
+ colorFrom: blue
5
+ colorTo: purple
6
+ sdk: docker
7
+ pinned: false
8
+ license: apache
9
+ ---
10
+
11
+ # SolverForge Quickstart
12
+
13
+ A constraint optimization application powered by [SolverForge](https://github.com/solverforge).
14
+
15
+ ## What This Does
16
+
17
+ This app solves a **task assignment optimization problem**:
18
+ - Assign tasks to resources while respecting skill requirements
19
+ - Balance workload fairly across resources
20
+ - Minimize total duration
21
+
22
+ ## How to Use
23
+
24
+ 1. Click "Load Small Dataset" to load sample data
25
+ 2. Click "Solve" to start the optimization
26
+ 3. Watch the score improve as the solver works
27
+ 4. Click "Stop" to terminate early if needed
28
+
29
+ ## API
30
+
31
+ Access the API documentation at `/q/swagger-ui`
32
+
33
+ ### Endpoints
34
+
35
+ | Method | Endpoint | Description |
36
+ |--------|----------|-------------|
37
+ | GET | `/demo-data` | List demo datasets |
38
+ | GET | `/demo-data/{id}` | Get demo data |
39
+ | POST | `/schedules` | Start solving |
40
+ | GET | `/schedules/{id}` | Get solution |
41
+ | DELETE | `/schedules/{id}` | Stop solving |
42
+
43
+ ## Technical Details
44
+
45
+ - **Backend**: Python + FastAPI
46
+ - **Solver**: SolverForge (Timefold-based metaheuristic)
47
+ - **Runtime**: Requires JDK 21 (included in Docker image)
logging.conf ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [loggers]
2
+ keys=root,solverforge
3
+
4
+ [handlers]
5
+ keys=consoleHandler
6
+
7
+ [formatters]
8
+ keys=simpleFormatter
9
+
10
+ [logger_root]
11
+ level=INFO
12
+ handlers=consoleHandler
13
+
14
+ [logger_solverforge]
15
+ level=INFO
16
+ qualname=solverforge
17
+ handlers=consoleHandler
18
+ propagate=0
19
+
20
+ [handler_consoleHandler]
21
+ class=StreamHandler
22
+ level=INFO
23
+ formatter=simpleFormatter
24
+ args=(sys.stdout,)
25
+
26
+ [formatter_simpleFormatter]
27
+ class=uvicorn.logging.ColourizedFormatter
28
+ format={levelprefix:<8} @ {name} : {message}
29
+ style={
30
+ use_colors=True
pyproject.toml ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+
6
+ [project]
7
+ name = "my_quickstart" # TODO: Change to your project name
8
+ version = "1.0.0"
9
+ description = "A SolverForge constraint optimization quickstart"
10
+ requires-python = ">=3.10"
11
+ dependencies = [
12
+ 'solverforge-legacy == 1.24.1',
13
+ 'fastapi == 0.111.0',
14
+ 'pydantic == 2.7.3',
15
+ 'uvicorn == 0.30.1',
16
+ ]
17
+
18
+ [project.optional-dependencies]
19
+ dev = [
20
+ 'pytest == 8.2.2',
21
+ ]
22
+
23
+ [project.scripts]
24
+ run-app = "my_quickstart:main"
src/my_quickstart/__init__.py ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import uvicorn
2
+ import os
3
+
4
+ from .rest_api import app as app
5
+
6
+
7
+ def main():
8
+ port = int(os.environ.get("PORT", 8080))
9
+ config = uvicorn.Config(
10
+ "my_quickstart:app",
11
+ host="0.0.0.0",
12
+ port=port,
13
+ log_config="logging.conf",
14
+ use_colors=True,
15
+ )
16
+ server = uvicorn.Server(config)
17
+ server.run()
18
+
19
+
20
+ if __name__ == "__main__":
21
+ main()
src/my_quickstart/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (865 Bytes). View file
 
src/my_quickstart/__pycache__/constraints.cpython-312.pyc ADDED
Binary file (9.05 kB). View file
 
src/my_quickstart/__pycache__/demo_data.cpython-312.pyc ADDED
Binary file (3.56 kB). View file
 
src/my_quickstart/__pycache__/domain.cpython-312.pyc ADDED
Binary file (5.76 kB). View file
 
src/my_quickstart/__pycache__/json_serialization.cpython-312.pyc ADDED
Binary file (1.79 kB). View file
 
src/my_quickstart/__pycache__/rest_api.cpython-312.pyc ADDED
Binary file (11.3 kB). View file
 
src/my_quickstart/__pycache__/solver.cpython-312.pyc ADDED
Binary file (1.31 kB). View file
 
src/my_quickstart/constraints.py ADDED
@@ -0,0 +1,239 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Constraint definitions for your optimization problem.
3
+
4
+ Constraints are defined using a fluent API:
5
+ 1. for_each(Entity) - iterate over all entities
6
+ 2. filter(predicate) - keep only matching entities
7
+ 3. join(OtherEntity, ...) - combine with other entities
8
+ 4. group_by(key, collector) - aggregate by key
9
+ 5. penalize(weight) or reward(weight) - affect the score
10
+
11
+ TODO: Replace these example constraints with your own business rules.
12
+ """
13
+
14
+ from solverforge_legacy.solver.score import (
15
+ constraint_provider,
16
+ ConstraintFactory,
17
+ Joiners,
18
+ HardSoftScore,
19
+ ConstraintCollectors,
20
+ )
21
+
22
+ from .domain import Resource, Task
23
+
24
+
25
+ # =============================================================================
26
+ # CONSTRAINT WEIGHTS
27
+ # =============================================================================
28
+ # Global weights that can be adjusted at runtime via the REST API.
29
+ # Weight 0 = disabled, 100 = full strength.
30
+ # Set by rest_api.py before solving starts.
31
+
32
+ CONSTRAINT_WEIGHTS = {
33
+ 'required_skill': 100, # Hard constraint
34
+ 'resource_capacity': 100, # Hard constraint
35
+ 'minimize_duration': 50, # Soft constraint
36
+ 'balance_load': 50, # Soft constraint
37
+ }
38
+
39
+
40
+ def get_weight(name: str) -> int:
41
+ """Get the weight for a constraint (0-100 scale)."""
42
+ return CONSTRAINT_WEIGHTS.get(name, 100)
43
+
44
+
45
+ @constraint_provider
46
+ def define_constraints(constraint_factory: ConstraintFactory):
47
+ """
48
+ Define all constraints for the optimization problem.
49
+
50
+ Returns a list of constraints, evaluated in order:
51
+ - Hard constraints: Must be satisfied (score < 0 = infeasible)
52
+ - Soft constraints: Should be optimized (higher = better)
53
+ """
54
+ return [
55
+ # Hard constraints (must be satisfied)
56
+ required_skill(constraint_factory),
57
+ resource_capacity(constraint_factory),
58
+
59
+ # Soft constraints (optimize these)
60
+ minimize_total_duration(constraint_factory),
61
+ balance_resource_load(constraint_factory),
62
+ ]
63
+
64
+
65
+ # =============================================================================
66
+ # HARD CONSTRAINTS
67
+ # =============================================================================
68
+
69
+ def required_skill(constraint_factory: ConstraintFactory):
70
+ """
71
+ Hard: Each task must be assigned to a resource with the required skill.
72
+
73
+ Pattern: for_each -> filter -> penalize
74
+
75
+ NOTE: We check task.resource is not None FIRST, because unassigned tasks
76
+ should not be penalized - they're just not yet assigned.
77
+
78
+ NOTE: We use len(str(task.required_skill)) > 0 instead of just task.required_skill
79
+ because the value may be a Java String object which doesn't work with Python's
80
+ boolean operators directly.
81
+
82
+ WEIGHT: When weight=0, this constraint is effectively disabled.
83
+ """
84
+ weight = get_weight('required_skill')
85
+ if weight == 0:
86
+ # Return a no-op constraint when disabled
87
+ return (
88
+ constraint_factory.for_each(Task)
89
+ .filter(lambda task: False) # Never matches
90
+ .penalize(HardSoftScore.ONE_HARD)
91
+ .as_constraint("Required skill missing")
92
+ )
93
+
94
+ return (
95
+ constraint_factory.for_each(Task)
96
+ .filter(lambda task: task.resource is not None
97
+ and len(str(task.required_skill)) > 0
98
+ and not task.has_required_skill())
99
+ .penalize(HardSoftScore.ONE_HARD, lambda task: weight)
100
+ .as_constraint("Required skill missing")
101
+ )
102
+
103
+
104
+ def resource_capacity(constraint_factory: ConstraintFactory):
105
+ """
106
+ Hard: Total task duration per resource must not exceed capacity.
107
+
108
+ Pattern: for_each -> group_by -> filter -> penalize
109
+
110
+ WEIGHT: When weight=0, this constraint is effectively disabled.
111
+ """
112
+ weight = get_weight('resource_capacity')
113
+ if weight == 0:
114
+ return (
115
+ constraint_factory.for_each(Task)
116
+ .filter(lambda task: False)
117
+ .penalize(HardSoftScore.ONE_HARD)
118
+ .as_constraint("Resource capacity exceeded")
119
+ )
120
+
121
+ return (
122
+ constraint_factory.for_each(Task)
123
+ .group_by(
124
+ lambda task: task.resource,
125
+ ConstraintCollectors.sum(lambda task: task.duration)
126
+ )
127
+ .filter(lambda resource, total_duration:
128
+ resource is not None and total_duration > resource.capacity)
129
+ .penalize(
130
+ HardSoftScore.ONE_HARD,
131
+ lambda resource, total_duration: (total_duration - resource.capacity) * weight // 100
132
+ )
133
+ .as_constraint("Resource capacity exceeded")
134
+ )
135
+
136
+
137
+ # =============================================================================
138
+ # SOFT CONSTRAINTS
139
+ # =============================================================================
140
+
141
+ def minimize_total_duration(constraint_factory: ConstraintFactory):
142
+ """
143
+ Soft: Prefer shorter total duration (makespan).
144
+
145
+ Pattern: for_each -> penalize with weight function
146
+
147
+ WEIGHT: Penalty multiplied by weight/100.
148
+ """
149
+ weight = get_weight('minimize_duration')
150
+ if weight == 0:
151
+ return (
152
+ constraint_factory.for_each(Task)
153
+ .filter(lambda task: False)
154
+ .penalize(HardSoftScore.ONE_SOFT)
155
+ .as_constraint("Minimize total duration")
156
+ )
157
+
158
+ return (
159
+ constraint_factory.for_each(Task)
160
+ .filter(lambda task: task.resource is not None)
161
+ .penalize(HardSoftScore.ONE_SOFT, lambda task: task.duration * weight // 100)
162
+ .as_constraint("Minimize total duration")
163
+ )
164
+
165
+
166
+ def balance_resource_load(constraint_factory: ConstraintFactory):
167
+ """
168
+ Soft: Balance workload fairly across all resources.
169
+
170
+ Pattern: for_each -> group_by -> complement -> group_by(loadBalance) -> penalize
171
+
172
+ WEIGHT: Penalty multiplied by weight/100.
173
+ """
174
+ weight = get_weight('balance_load')
175
+ if weight == 0:
176
+ return (
177
+ constraint_factory.for_each(Task)
178
+ .filter(lambda task: False)
179
+ .penalize(HardSoftScore.ONE_SOFT)
180
+ .as_constraint("Balance resource load")
181
+ )
182
+
183
+ return (
184
+ constraint_factory.for_each(Task)
185
+ .group_by(
186
+ lambda task: task.resource,
187
+ ConstraintCollectors.sum(lambda task: task.duration)
188
+ )
189
+ .complement(Resource, lambda r: 0) # Include resources with 0 tasks
190
+ .group_by(
191
+ ConstraintCollectors.load_balance(
192
+ lambda resource, duration: resource,
193
+ lambda resource, duration: duration,
194
+ )
195
+ )
196
+ .penalize(
197
+ HardSoftScore.ONE_SOFT,
198
+ lambda load_balance: int(load_balance.unfairness()) * weight // 100
199
+ )
200
+ .as_constraint("Balance resource load")
201
+ )
202
+
203
+
204
+ # =============================================================================
205
+ # ADDITIONAL CONSTRAINT PATTERNS (commented examples)
206
+ # =============================================================================
207
+
208
+ # def no_overlapping_tasks(constraint_factory: ConstraintFactory):
209
+ # """
210
+ # Example: Two tasks on same resource cannot overlap in time.
211
+ #
212
+ # Pattern: for_each_unique_pair with Joiners
213
+ # """
214
+ # return (
215
+ # constraint_factory.for_each_unique_pair(
216
+ # Task,
217
+ # Joiners.equal(lambda task: task.resource),
218
+ # Joiners.overlapping(
219
+ # lambda task: task.start_time,
220
+ # lambda task: task.end_time
221
+ # ),
222
+ # )
223
+ # .penalize(HardSoftScore.ONE_HARD)
224
+ # .as_constraint("Overlapping tasks")
225
+ # )
226
+
227
+
228
+ # def preferred_resource(constraint_factory: ConstraintFactory):
229
+ # """
230
+ # Example: Reward tasks assigned to their preferred resource.
231
+ #
232
+ # Pattern: for_each -> filter -> reward
233
+ # """
234
+ # return (
235
+ # constraint_factory.for_each(Task)
236
+ # .filter(lambda task: task.resource == task.preferred_resource)
237
+ # .reward(HardSoftScore.ONE_SOFT)
238
+ # .as_constraint("Preferred resource")
239
+ # )
src/my_quickstart/demo_data.py ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Demo data generation.
3
+
4
+ Provides sample datasets for testing and demonstration.
5
+ TODO: Replace with realistic data for your domain.
6
+ """
7
+
8
+ from enum import Enum
9
+ from .domain import Resource, Task, Schedule
10
+
11
+
12
+ class DemoData(str, Enum):
13
+ """Available demo datasets."""
14
+ SMALL = "SMALL"
15
+ MEDIUM = "MEDIUM"
16
+
17
+
18
+ def generate_demo_data(dataset: DemoData) -> Schedule:
19
+ """
20
+ Generate a demo dataset.
21
+
22
+ TODO: Replace with realistic data for your domain.
23
+ """
24
+ if dataset == DemoData.SMALL:
25
+ return _generate_small()
26
+ elif dataset == DemoData.MEDIUM:
27
+ return _generate_medium()
28
+ else:
29
+ raise ValueError(f"Unknown dataset: {dataset}")
30
+
31
+
32
+ def _generate_small() -> Schedule:
33
+ """Small dataset: 3 resources, 10 tasks."""
34
+ resources = [
35
+ Resource(name="Alice", capacity=100, skills={"python", "sql"}),
36
+ Resource(name="Bob", capacity=120, skills={"python", "java"}),
37
+ Resource(name="Charlie", capacity=80, skills={"sql", "java"}),
38
+ ]
39
+
40
+ tasks = [
41
+ Task(id="task-1", name="Data Pipeline", duration=30, required_skill="python"),
42
+ Task(id="task-2", name="API Development", duration=45, required_skill="python"),
43
+ Task(id="task-3", name="Database Schema", duration=20, required_skill="sql"),
44
+ Task(id="task-4", name="Query Optimization", duration=35, required_skill="sql"),
45
+ Task(id="task-5", name="Backend Service", duration=50, required_skill="java"),
46
+ Task(id="task-6", name="Data Analysis", duration=25, required_skill="python"),
47
+ Task(id="task-7", name="Report Generation", duration=15, required_skill="sql"),
48
+ Task(id="task-8", name="Integration Tests", duration=40, required_skill="java"),
49
+ Task(id="task-9", name="Code Review", duration=20), # No skill required
50
+ Task(id="task-10", name="Documentation", duration=15), # No skill required
51
+ ]
52
+
53
+ return Schedule(resources=resources, tasks=tasks)
54
+
55
+
56
+ def _generate_medium() -> Schedule:
57
+ """Medium dataset: 5 resources, 25 tasks.
58
+
59
+ Total capacity: 700 min (150+140+130+160+120)
60
+ Total task duration: ~675 min (feasible but challenging)
61
+ """
62
+ resources = [
63
+ Resource(name="Alice", capacity=150, skills={"python", "sql", "ml"}),
64
+ Resource(name="Bob", capacity=140, skills={"python", "java", "devops"}),
65
+ Resource(name="Charlie", capacity=130, skills={"sql", "java", "frontend"}),
66
+ Resource(name="Diana", capacity=160, skills={"python", "ml", "devops"}),
67
+ Resource(name="Eve", capacity=120, skills={"frontend", "java", "sql"}),
68
+ ]
69
+
70
+ skills = ["python", "sql", "java", "ml", "devops", "frontend", ""]
71
+ tasks = []
72
+ for i in range(25):
73
+ skill = skills[i % len(skills)]
74
+ tasks.append(
75
+ Task(
76
+ id=f"task-{i+1}",
77
+ name=f"Task {i+1}",
78
+ # Duration formula: 15-39 min, total ~675 min (fits in 700 capacity)
79
+ duration=15 + (i * 3) % 25,
80
+ required_skill=skill,
81
+ )
82
+ )
83
+
84
+ return Schedule(resources=resources, tasks=tasks)
src/my_quickstart/domain.py ADDED
@@ -0,0 +1,141 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Domain model for your optimization problem.
3
+
4
+ This file defines:
5
+ 1. Problem facts (@dataclass) - immutable input data
6
+ 2. Planning entities (@planning_entity) - what the solver assigns
7
+ 3. Planning solution (@planning_solution) - container for the problem
8
+
9
+ TODO: Replace this example with your own domain model.
10
+ """
11
+
12
+ from dataclasses import dataclass, field
13
+ from typing import Annotated, Optional, List
14
+ from datetime import datetime
15
+
16
+ from solverforge_legacy.solver import SolverStatus
17
+ from solverforge_legacy.solver.domain import (
18
+ planning_entity,
19
+ planning_solution,
20
+ PlanningId,
21
+ PlanningVariable,
22
+ PlanningEntityCollectionProperty,
23
+ ProblemFactCollectionProperty,
24
+ ValueRangeProvider,
25
+ PlanningScore,
26
+ )
27
+ from solverforge_legacy.solver.score import HardSoftScore
28
+
29
+ from .json_serialization import JsonDomainBase
30
+ from pydantic import Field
31
+
32
+
33
+ # =============================================================================
34
+ # PROBLEM FACTS (immutable input data)
35
+ # =============================================================================
36
+
37
+ @dataclass
38
+ class Resource:
39
+ """
40
+ A resource that can be assigned to tasks.
41
+
42
+ TODO: Replace with your own problem fact (e.g., Employee, Room, Vehicle).
43
+ """
44
+ name: Annotated[str, PlanningId]
45
+ capacity: int = 100
46
+ skills: set[str] = field(default_factory=set)
47
+
48
+
49
+ # =============================================================================
50
+ # PLANNING ENTITIES (what the solver optimizes)
51
+ # =============================================================================
52
+
53
+ @planning_entity
54
+ @dataclass
55
+ class Task:
56
+ """
57
+ A task to be assigned to a resource.
58
+
59
+ The `resource` field is the planning variable - the solver will
60
+ try different assignments to find the best solution.
61
+
62
+ TODO: Replace with your own planning entity (e.g., Shift, Lesson, Delivery).
63
+ """
64
+ id: Annotated[str, PlanningId]
65
+ name: str
66
+ duration: int # in minutes
67
+ required_skill: str = ""
68
+
69
+ # This is the planning variable - solver assigns this
70
+ resource: Annotated[Resource | None, PlanningVariable] = None
71
+
72
+ def has_required_skill(self) -> bool:
73
+ """Check if assigned resource has the required skill.
74
+
75
+ NOTE: We use len(str(...)) instead of boolean check because
76
+ required_skill may be a Java String during constraint evaluation.
77
+ """
78
+ if self.resource is None:
79
+ return False
80
+ if len(str(self.required_skill)) == 0:
81
+ return True
82
+ return str(self.required_skill) in self.resource.skills
83
+
84
+
85
+ # =============================================================================
86
+ # PLANNING SOLUTION (container)
87
+ # =============================================================================
88
+
89
+ @planning_solution
90
+ @dataclass
91
+ class Schedule:
92
+ """
93
+ The planning solution containing all problem facts and planning entities.
94
+
95
+ TODO: Rename to match your domain (e.g., Timetable, RoutePlan, Roster).
96
+ """
97
+ resources: Annotated[
98
+ list[Resource],
99
+ ProblemFactCollectionProperty,
100
+ ValueRangeProvider
101
+ ]
102
+ tasks: Annotated[list[Task], PlanningEntityCollectionProperty]
103
+ score: Annotated[HardSoftScore | None, PlanningScore] = None
104
+ solver_status: SolverStatus = SolverStatus.NOT_SOLVING
105
+
106
+
107
+ # =============================================================================
108
+ # PYDANTIC MODELS (for REST API serialization)
109
+ # =============================================================================
110
+
111
+ class ResourceModel(JsonDomainBase):
112
+ """Pydantic model for Resource serialization."""
113
+ name: str
114
+ capacity: int = 100
115
+ skills: List[str] = Field(default_factory=list)
116
+
117
+
118
+ class TaskModel(JsonDomainBase):
119
+ """Pydantic model for Task serialization."""
120
+ id: str
121
+ name: str
122
+ duration: int
123
+ required_skill: str = Field(default="", alias="requiredSkill")
124
+ resource: Optional[str] = None # Resource name or None
125
+
126
+
127
+ class ConstraintWeightsModel(JsonDomainBase):
128
+ """Pydantic model for constraint weight configuration."""
129
+ required_skill: int = Field(default=100, ge=0, le=100)
130
+ resource_capacity: int = Field(default=100, ge=0, le=100)
131
+ minimize_duration: int = Field(default=50, ge=0, le=100)
132
+ balance_load: int = Field(default=50, ge=0, le=100)
133
+
134
+
135
+ class ScheduleModel(JsonDomainBase):
136
+ """Pydantic model for Schedule serialization."""
137
+ resources: List[ResourceModel]
138
+ tasks: List[TaskModel]
139
+ score: Optional[str] = None
140
+ solver_status: Optional[str] = Field(default=None, alias="solverStatus")
141
+ constraint_weights: Optional[ConstraintWeightsModel] = Field(default=None, alias="constraintWeights")
src/my_quickstart/json_serialization.py ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Pydantic utilities for JSON serialization.
3
+
4
+ This provides a base class with camelCase conversion for REST API compatibility.
5
+ """
6
+
7
+ from solverforge_legacy.solver.score import HardSoftScore
8
+ from typing import Any
9
+ from pydantic import BaseModel, ConfigDict, PlainSerializer, BeforeValidator
10
+ from pydantic.alias_generators import to_camel
11
+
12
+
13
+ # Score serialization (HardSoftScore -> string like "0hard/0soft")
14
+ ScoreSerializer = PlainSerializer(
15
+ lambda score: str(score) if score is not None else None,
16
+ return_type=str | None
17
+ )
18
+
19
+
20
+ def validate_score(v: Any) -> Any:
21
+ """Parse score from string format."""
22
+ if isinstance(v, HardSoftScore) or v is None:
23
+ return v
24
+ if isinstance(v, str):
25
+ return HardSoftScore.parse(v)
26
+ raise ValueError('"score" should be a string')
27
+
28
+
29
+ ScoreValidator = BeforeValidator(validate_score)
30
+
31
+
32
+ class JsonDomainBase(BaseModel):
33
+ """
34
+ Base class for Pydantic models with camelCase JSON serialization.
35
+
36
+ Converts Python snake_case to JavaScript camelCase automatically.
37
+ """
38
+ model_config = ConfigDict(
39
+ alias_generator=to_camel,
40
+ populate_by_name=True,
41
+ from_attributes=True,
42
+ )
src/my_quickstart/rest_api.py ADDED
@@ -0,0 +1,285 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ REST API endpoints.
3
+
4
+ Provides a standard API for:
5
+ - Listing and fetching demo data
6
+ - Starting/stopping solving
7
+ - Getting solution status
8
+ - Analyzing scores
9
+ """
10
+
11
+ from fastapi import FastAPI
12
+ from fastapi.staticfiles import StaticFiles
13
+ from uuid import uuid4
14
+ from dataclasses import replace
15
+ from typing import Dict, List
16
+ import os
17
+
18
+ from .domain import Schedule, ScheduleModel, Resource, Task, ConstraintWeightsModel
19
+ from .demo_data import DemoData, generate_demo_data
20
+ from .solver import solver_manager, solution_manager
21
+ from . import constraints # For setting global weights
22
+
23
+
24
+ app = FastAPI(
25
+ title="SolverForge Quickstart",
26
+ description="Constraint optimization API",
27
+ docs_url='/q/swagger-ui'
28
+ )
29
+
30
+ # In-memory storage for solving jobs
31
+ data_sets: dict[str, Schedule] = {}
32
+
33
+
34
+ # =============================================================================
35
+ # DEMO DATA ENDPOINTS
36
+ # =============================================================================
37
+
38
+ @app.get("/demo-data")
39
+ async def demo_data_list() -> list[DemoData]:
40
+ """List available demo datasets."""
41
+ return [e for e in DemoData]
42
+
43
+
44
+ @app.get("/demo-data/{dataset_id}", response_model_exclude_none=True)
45
+ async def get_demo_data(dataset_id: str) -> ScheduleModel:
46
+ """Get a specific demo dataset."""
47
+ demo_data = getattr(DemoData, dataset_id)
48
+ domain_schedule = generate_demo_data(demo_data)
49
+ return _schedule_to_model(domain_schedule)
50
+
51
+
52
+ # =============================================================================
53
+ # SOLVING ENDPOINTS
54
+ # =============================================================================
55
+
56
+ @app.post("/schedules")
57
+ async def solve(schedule_model: ScheduleModel) -> str:
58
+ """
59
+ Start solving a schedule.
60
+
61
+ Returns a job ID that can be used to check progress and get results.
62
+ Accepts optional constraint_weights to adjust constraint penalties.
63
+ """
64
+ job_id = str(uuid4())
65
+
66
+ # Set constraint weights globally before solving
67
+ if schedule_model.constraint_weights:
68
+ weights = schedule_model.constraint_weights
69
+ constraints.CONSTRAINT_WEIGHTS = {
70
+ 'required_skill': weights.required_skill,
71
+ 'resource_capacity': weights.resource_capacity,
72
+ 'minimize_duration': weights.minimize_duration,
73
+ 'balance_load': weights.balance_load,
74
+ }
75
+ else:
76
+ # Reset to defaults
77
+ constraints.CONSTRAINT_WEIGHTS = {
78
+ 'required_skill': 100,
79
+ 'resource_capacity': 100,
80
+ 'minimize_duration': 50,
81
+ 'balance_load': 50,
82
+ }
83
+
84
+ schedule = _model_to_schedule(schedule_model)
85
+ data_sets[job_id] = schedule
86
+
87
+ solver_manager.solve_and_listen(
88
+ job_id,
89
+ schedule,
90
+ lambda solution: _update_schedule(job_id, solution)
91
+ )
92
+ return job_id
93
+
94
+
95
+ @app.get("/schedules")
96
+ async def list_schedules() -> List[str]:
97
+ """List all job IDs."""
98
+ return list(data_sets.keys())
99
+
100
+
101
+ @app.get("/schedules/{job_id}", response_model_exclude_none=True)
102
+ async def get_schedule(job_id: str) -> ScheduleModel:
103
+ """Get the current solution for a job."""
104
+ if job_id not in data_sets:
105
+ raise ValueError(f"No schedule found with ID {job_id}")
106
+
107
+ schedule = data_sets[job_id]
108
+ updated = replace(schedule, solver_status=solver_manager.get_solver_status(job_id))
109
+ return _schedule_to_model(updated)
110
+
111
+
112
+ @app.get("/schedules/{job_id}/status")
113
+ async def get_status(job_id: str) -> Dict:
114
+ """Get solving status and score."""
115
+ if job_id not in data_sets:
116
+ raise ValueError(f"No schedule found with ID {job_id}")
117
+
118
+ schedule = data_sets[job_id]
119
+ solver_status = solver_manager.get_solver_status(job_id)
120
+
121
+ return {
122
+ "score": {
123
+ "hardScore": schedule.score.hard_score if schedule.score else 0,
124
+ "softScore": schedule.score.soft_score if schedule.score else 0,
125
+ },
126
+ "solverStatus": solver_status.name,
127
+ }
128
+
129
+
130
+ @app.delete("/schedules/{job_id}")
131
+ async def stop_solving(job_id: str) -> ScheduleModel:
132
+ """Stop solving and return current solution."""
133
+ if job_id not in data_sets:
134
+ raise ValueError(f"No schedule found with ID {job_id}")
135
+
136
+ try:
137
+ solver_manager.terminate_early(job_id)
138
+ except Exception as e:
139
+ print(f"Warning: terminate_early failed for {job_id}: {e}")
140
+
141
+ return await get_schedule(job_id)
142
+
143
+
144
+ @app.put("/schedules/analyze")
145
+ async def analyze(schedule_model: ScheduleModel) -> Dict:
146
+ """Analyze a schedule's score breakdown."""
147
+ schedule = _model_to_schedule(schedule_model)
148
+ analysis = solution_manager.analyze(schedule)
149
+
150
+ constraints = []
151
+ for constraint in getattr(analysis, 'constraint_analyses', []) or []:
152
+ matches = [
153
+ {
154
+ "name": str(getattr(getattr(match, 'constraint_ref', None), 'constraint_name', "")),
155
+ "score": str(getattr(match, 'score', "0hard/0soft")),
156
+ "justification": str(getattr(match, 'justification', "")),
157
+ }
158
+ for match in getattr(constraint, 'matches', []) or []
159
+ ]
160
+ constraints.append({
161
+ "name": str(getattr(constraint, 'constraint_name', "")),
162
+ "weight": str(getattr(constraint, 'weight', "0hard/0soft")),
163
+ "score": str(getattr(constraint, 'score', "0hard/0soft")),
164
+ "matches": matches,
165
+ })
166
+
167
+ return {"constraints": constraints}
168
+
169
+
170
+ # =============================================================================
171
+ # HELPER FUNCTIONS
172
+ # =============================================================================
173
+
174
+ def _update_schedule(job_id: str, schedule: Schedule):
175
+ """Callback for solver updates."""
176
+ global data_sets
177
+ data_sets[job_id] = schedule
178
+
179
+
180
+ def _schedule_to_model(schedule: Schedule) -> ScheduleModel:
181
+ """Convert domain Schedule to Pydantic model."""
182
+ from .domain import ResourceModel, TaskModel, ScheduleModel
183
+
184
+ resources = [
185
+ ResourceModel(
186
+ name=r.name,
187
+ capacity=r.capacity,
188
+ skills=list(r.skills),
189
+ )
190
+ for r in schedule.resources
191
+ ]
192
+
193
+ tasks = [
194
+ TaskModel(
195
+ id=t.id,
196
+ name=t.name,
197
+ duration=t.duration,
198
+ requiredSkill=t.required_skill,
199
+ resource=t.resource.name if t.resource else None,
200
+ )
201
+ for t in schedule.tasks
202
+ ]
203
+
204
+ return ScheduleModel(
205
+ resources=resources,
206
+ tasks=tasks,
207
+ score=str(schedule.score) if schedule.score else None,
208
+ solverStatus=schedule.solver_status.name if schedule.solver_status else None,
209
+ )
210
+
211
+
212
+ def _model_to_schedule(model: ScheduleModel) -> Schedule:
213
+ """Convert Pydantic model to domain Schedule."""
214
+ resources = {
215
+ r.name: Resource(
216
+ name=r.name,
217
+ capacity=r.capacity,
218
+ skills=set(r.skills),
219
+ )
220
+ for r in model.resources
221
+ }
222
+
223
+ tasks = [
224
+ Task(
225
+ id=t.id,
226
+ name=t.name,
227
+ duration=t.duration,
228
+ required_skill=t.required_skill or "",
229
+ resource=resources.get(t.resource) if t.resource else None,
230
+ )
231
+ for t in model.tasks
232
+ ]
233
+
234
+ return Schedule(
235
+ resources=list(resources.values()),
236
+ tasks=tasks,
237
+ )
238
+
239
+
240
+ # =============================================================================
241
+ # SOURCE CODE VIEWER ENDPOINTS
242
+ # =============================================================================
243
+
244
+ # Whitelist of files that can be viewed
245
+ SOURCE_FILES = {
246
+ 'domain.py': 'src/my_quickstart/domain.py',
247
+ 'constraints.py': 'src/my_quickstart/constraints.py',
248
+ 'solver.py': 'src/my_quickstart/solver.py',
249
+ 'rest_api.py': 'src/my_quickstart/rest_api.py',
250
+ 'demo_data.py': 'src/my_quickstart/demo_data.py',
251
+ 'index.html': 'static/index.html',
252
+ 'app.js': 'static/app.js',
253
+ 'app.css': 'static/app.css',
254
+ }
255
+
256
+
257
+ @app.get("/source-code")
258
+ async def list_source_files() -> List[str]:
259
+ """List available source files for the code viewer."""
260
+ return list(SOURCE_FILES.keys())
261
+
262
+
263
+ @app.get("/source-code/{filename}")
264
+ async def get_source_code(filename: str) -> Dict:
265
+ """Get the contents of a source file."""
266
+ if filename not in SOURCE_FILES:
267
+ raise ValueError(f"File not available: {filename}")
268
+
269
+ filepath = SOURCE_FILES[filename]
270
+ if not os.path.exists(filepath):
271
+ raise ValueError(f"File not found: {filepath}")
272
+
273
+ with open(filepath, 'r') as f:
274
+ content = f.read()
275
+
276
+ return {"filename": filename, "content": content}
277
+
278
+
279
+ # =============================================================================
280
+ # STATIC FILES (optional web UI)
281
+ # =============================================================================
282
+
283
+ # Mount static files if directory exists
284
+ if os.path.exists("static"):
285
+ app.mount("/", StaticFiles(directory="static", html=True), name="static")
src/my_quickstart/solver.py ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Solver configuration.
3
+
4
+ This file configures the SolverForge optimization engine:
5
+ - Solution class (what to optimize)
6
+ - Entity classes (what to assign)
7
+ - Constraint provider (business rules)
8
+ - Termination config (when to stop)
9
+ """
10
+
11
+ from solverforge_legacy.solver import SolverManager, SolverFactory, SolutionManager
12
+ from solverforge_legacy.solver.config import (
13
+ SolverConfig,
14
+ ScoreDirectorFactoryConfig,
15
+ TerminationConfig,
16
+ Duration,
17
+ )
18
+
19
+ from .domain import Schedule, Task
20
+ from .constraints import define_constraints
21
+
22
+
23
+ # Solver configuration
24
+ solver_config = SolverConfig(
25
+ solution_class=Schedule,
26
+ entity_class_list=[Task],
27
+ score_director_factory_config=ScoreDirectorFactoryConfig(
28
+ constraint_provider_function=define_constraints
29
+ ),
30
+ termination_config=TerminationConfig(
31
+ # Stop after 30 seconds (adjust for your problem size)
32
+ spent_limit=Duration(seconds=30)
33
+ ),
34
+ )
35
+
36
+ # Create solver manager (handles async solving)
37
+ solver_manager = SolverManager.create(SolverFactory.create(solver_config))
38
+
39
+ # Create solution manager (for score analysis)
40
+ solution_manager = SolutionManager.create(solver_manager)
static/app.css ADDED
@@ -0,0 +1,728 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /*
2
+ * =============================================================================
3
+ * SOLVERFORGE QUICKSTART TEMPLATE - APP STYLES
4
+ * =============================================================================
5
+ *
6
+ * Custom styles for the quickstart template UI.
7
+ * This file creates the "code-link" educational UI appearance.
8
+ *
9
+ * CUSTOMIZATION GUIDE:
10
+ * - Brand colors: Change #10b981 (green) and #3E00FF (violet)
11
+ * - Card styles: Modify .kpi-card, .task-card, .resource-card
12
+ * - Animations: Adjust @keyframes rules for different effects
13
+ *
14
+ * SECTIONS:
15
+ * 1. Global styles
16
+ * 2. Interactive Code clickable elements (code reveal on hover)
17
+ * 3. Hero section (gradient banner)
18
+ * 4. KPI cards (metrics display)
19
+ * 5. Control bar (solve/stop buttons)
20
+ * 6. Section cards (container panels)
21
+ * 7. Task cards (planning entity visualization)
22
+ * 8. Resource cards (problem fact visualization)
23
+ * 9. Constraint legend (clickable constraint badges)
24
+ * 10. Build tab (file navigator and code viewer)
25
+ * 11. Navigation overrides
26
+ * 12. Responsive adjustments
27
+ */
28
+
29
+ /* =============================================================================
30
+ * 1. GLOBAL STYLES
31
+ *
32
+ * Base styles for the page. The light gray background (#f8fafc) provides
33
+ * contrast for white cards.
34
+ * ============================================================================= */
35
+ body {
36
+ background: #f8fafc;
37
+ }
38
+
39
+ /* =============================================================================
40
+ * 2. INTERACTIVE CODE CLICKABLE ELEMENTS
41
+ *
42
+ * The "code-link" feature: every UI element shows a code icon on hover,
43
+ * indicating it can be clicked to view the source code that generates it.
44
+ *
45
+ * HOW IT WORKS:
46
+ * 1. Add class="code-link" to any element
47
+ * 2. Add data-target="filename:section" to specify the code location
48
+ * 3. CSS ::after pseudo-element creates the code icon
49
+ * 4. JavaScript handles click -> navigates to Build tab
50
+ *
51
+ * EXAMPLE:
52
+ * <div class="kpi-card code-link" data-target="app.js:updateKPIs">
53
+ * <span class="code-tooltip">updateKPIs() in app.js</span>
54
+ * ...content...
55
+ * </div>
56
+ * ============================================================================= */
57
+ .code-link {
58
+ position: relative;
59
+ cursor: pointer;
60
+ transition: all 0.2s ease;
61
+ }
62
+
63
+ /* Code icon that appears on hover (uses Font Awesome code icon) */
64
+ .code-link::after {
65
+ content: '\f121'; /* fa-code unicode */
66
+ font-family: 'Font Awesome 6 Free';
67
+ font-weight: 900;
68
+ position: absolute;
69
+ top: 4px;
70
+ right: 4px;
71
+ width: 20px;
72
+ height: 20px;
73
+ background: rgba(62, 0, 255, 0.9); /* SolverForge violet */
74
+ color: white;
75
+ border-radius: 4px;
76
+ font-size: 10px;
77
+ display: flex;
78
+ align-items: center;
79
+ justify-content: center;
80
+ opacity: 0;
81
+ transform: scale(0.8);
82
+ transition: all 0.2s ease;
83
+ }
84
+
85
+ .code-link:hover::after {
86
+ opacity: 1;
87
+ transform: scale(1);
88
+ }
89
+
90
+ /* Violet glow border on hover to indicate clickability */
91
+ .code-link:hover {
92
+ box-shadow: 0 0 0 2px rgba(62, 0, 255, 0.5);
93
+ }
94
+
95
+ /* Tooltip that appears above code-link elements */
96
+ .code-tooltip {
97
+ position: absolute;
98
+ bottom: calc(100% + 8px);
99
+ left: 50%;
100
+ transform: translateX(-50%);
101
+ background: #1f2937;
102
+ color: white;
103
+ padding: 6px 12px;
104
+ border-radius: 6px;
105
+ font-size: 12px;
106
+ white-space: nowrap;
107
+ opacity: 0;
108
+ visibility: hidden;
109
+ transition: all 0.2s ease;
110
+ z-index: 1000;
111
+ pointer-events: none;
112
+ }
113
+
114
+ /* Tooltip arrow */
115
+ .code-tooltip::after {
116
+ content: '';
117
+ position: absolute;
118
+ top: 100%;
119
+ left: 50%;
120
+ transform: translateX(-50%);
121
+ border: 6px solid transparent;
122
+ border-top-color: #1f2937;
123
+ }
124
+
125
+ .code-link:hover .code-tooltip {
126
+ opacity: 1;
127
+ visibility: visible;
128
+ }
129
+
130
+ /* =============================================================================
131
+ * 3. HERO SECTION
132
+ *
133
+ * The prominent banner at the top of the Demo tab. Uses a gradient from
134
+ * SolverForge violet to green, with a rotating shimmer effect.
135
+ *
136
+ * TODO: Update the gradient colors to match your brand or problem domain.
137
+ * ============================================================================= */
138
+ .hero-section {
139
+ background: linear-gradient(135deg, #2E1760 0%, #3E00FF 50%, #10b981 100%);
140
+ position: relative;
141
+ overflow: hidden;
142
+ border-radius: 16px;
143
+ padding: 2rem;
144
+ }
145
+
146
+ /* Animated shimmer effect - creates visual interest */
147
+ .hero-section::before {
148
+ content: '';
149
+ position: absolute;
150
+ top: -50%;
151
+ right: -50%;
152
+ width: 100%;
153
+ height: 200%;
154
+ background: radial-gradient(circle, rgba(255,255,255,0.1) 0%, transparent 70%);
155
+ animation: shimmer 15s infinite linear;
156
+ }
157
+
158
+ @keyframes shimmer {
159
+ 0% { transform: rotate(0deg); }
160
+ 100% { transform: rotate(360deg); }
161
+ }
162
+
163
+ /* Small badge above the hero title */
164
+ .hero-badge {
165
+ background: rgba(255,255,255,0.2);
166
+ color: white;
167
+ padding: 4px 12px;
168
+ border-radius: 20px;
169
+ font-size: 12px;
170
+ display: inline-block;
171
+ margin-bottom: 1rem;
172
+ }
173
+
174
+ /* =============================================================================
175
+ * 4. KPI CARDS
176
+ *
177
+ * Key Performance Indicator cards show solution metrics at a glance.
178
+ *
179
+ * DEFAULT KPIs (customize for your domain):
180
+ * - Total Tasks: Count of planning entities
181
+ * - Assigned: Count with non-null planning variable
182
+ * - Violations: Count of hard constraint violations
183
+ * - Score: The HardSoftScore from the solver
184
+ *
185
+ * COLOR CODING:
186
+ * - Purple (#6366f1): Neutral/informational
187
+ * - Green (#10b981): Positive/success
188
+ * - Red (#ef4444): Negative/violations
189
+ * - Violet (#3E00FF): Score/brand
190
+ * ============================================================================= */
191
+ .kpi-card {
192
+ background: white;
193
+ border-radius: 12px;
194
+ padding: 1.25rem;
195
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
196
+ text-align: center;
197
+ transition: transform 0.2s, box-shadow 0.2s;
198
+ height: 100%;
199
+ }
200
+
201
+ /* Hover lift effect - makes cards feel interactive */
202
+ .kpi-card:hover {
203
+ transform: translateY(-4px);
204
+ box-shadow: 0 12px 20px -5px rgba(0, 0, 0, 0.15);
205
+ }
206
+
207
+ .kpi-icon {
208
+ font-size: 1.5rem;
209
+ margin-bottom: 0.5rem;
210
+ opacity: 0.8;
211
+ }
212
+
213
+ .kpi-value {
214
+ font-size: 2rem;
215
+ font-weight: 700;
216
+ line-height: 1.2;
217
+ }
218
+
219
+ .kpi-label {
220
+ font-size: 0.75rem;
221
+ color: #6b7280;
222
+ text-transform: uppercase;
223
+ letter-spacing: 0.05em;
224
+ margin-top: 0.25rem;
225
+ }
226
+
227
+ /* KPI color variations - each card type has its own color */
228
+ .kpi-tasks .kpi-icon, .kpi-tasks .kpi-value { color: #6366f1; }
229
+ .kpi-assigned .kpi-icon, .kpi-assigned .kpi-value { color: #10b981; }
230
+ .kpi-violations .kpi-icon, .kpi-violations .kpi-value { color: #ef4444; }
231
+ .kpi-score .kpi-icon, .kpi-score .kpi-value { color: #3E00FF; }
232
+
233
+ /* Pulse animation when KPI value changes (applied via JavaScript) */
234
+ .kpi-pulse {
235
+ animation: kpiPulse 0.5s ease;
236
+ }
237
+
238
+ @keyframes kpiPulse {
239
+ 0%, 100% { transform: scale(1); }
240
+ 50% { transform: scale(1.05); }
241
+ }
242
+
243
+ /* =============================================================================
244
+ * 5. CONTROL BAR
245
+ *
246
+ * Contains the Solve/Stop buttons and solving spinner.
247
+ *
248
+ * BUTTONS:
249
+ * - #solveButton: Starts the solver (POST to /schedules)
250
+ * - #stopSolvingButton: Stops the solver (DELETE to /schedules/{id})
251
+ * - #analyzeButton: Shows score breakdown (PUT to /schedules/analyze)
252
+ *
253
+ * The spinner (#solvingSpinner) animates while solving is active.
254
+ * ============================================================================= */
255
+ .control-bar {
256
+ background: white;
257
+ border-radius: 12px;
258
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
259
+ padding: 1rem 1.5rem;
260
+ }
261
+
262
+ /* Solving spinner - hidden by default, shown when solving */
263
+ #solvingSpinner {
264
+ display: none;
265
+ width: 24px;
266
+ height: 24px;
267
+ border: 3px solid #10b981;
268
+ border-top-color: transparent;
269
+ border-radius: 50%;
270
+ animation: spin 0.75s linear infinite;
271
+ }
272
+
273
+ #solvingSpinner.active {
274
+ display: inline-block;
275
+ }
276
+
277
+ @keyframes spin {
278
+ to { transform: rotate(360deg); }
279
+ }
280
+
281
+ /* =============================================================================
282
+ * 6. SECTION CARDS
283
+ *
284
+ * Container panels for grouping content (Resources, Tasks, etc.).
285
+ * Consistent styling with header and body areas.
286
+ * ============================================================================= */
287
+ .section-card {
288
+ background: white;
289
+ border-radius: 12px;
290
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
291
+ overflow: hidden;
292
+ height: 100%;
293
+ }
294
+
295
+ .section-header {
296
+ background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
297
+ padding: 1rem 1.25rem;
298
+ border-bottom: 1px solid #e2e8f0;
299
+ display: flex;
300
+ justify-content: space-between;
301
+ align-items: center;
302
+ }
303
+
304
+ .section-header h5 {
305
+ margin: 0;
306
+ font-size: 1rem;
307
+ font-weight: 600;
308
+ color: #334155;
309
+ }
310
+
311
+ .section-body {
312
+ padding: 1.25rem;
313
+ }
314
+
315
+ /* =============================================================================
316
+ * 7. TASK CARDS
317
+ *
318
+ * Visualization for planning entities (what the solver assigns).
319
+ *
320
+ * STATES:
321
+ * - Default (green border): Task is assigned
322
+ * - .unassigned (orange): Task has no resource assigned
323
+ * - .violation (red): Task has a constraint violation
324
+ *
325
+ * CUSTOMIZATION:
326
+ * Modify createTaskCard() in app.js to change what's displayed.
327
+ * ============================================================================= */
328
+ .task-grid {
329
+ display: grid;
330
+ grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
331
+ gap: 1rem;
332
+ }
333
+
334
+ .task-card {
335
+ background: #f8fafc;
336
+ border-radius: 10px;
337
+ padding: 1rem;
338
+ border-left: 4px solid #10b981; /* Green = assigned */
339
+ transition: all 0.2s ease;
340
+ }
341
+
342
+ .task-card:hover {
343
+ transform: translateY(-2px);
344
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
345
+ }
346
+
347
+ /* Unassigned state - yellow/orange warning */
348
+ .task-card.unassigned {
349
+ border-left-color: #f59e0b;
350
+ background: #fffbeb;
351
+ }
352
+
353
+ /* Violation state - red error */
354
+ .task-card.violation {
355
+ border-left-color: #ef4444;
356
+ background: #fef2f2;
357
+ }
358
+
359
+ .task-name {
360
+ font-weight: 600;
361
+ color: #1e293b;
362
+ margin-bottom: 0.5rem;
363
+ display: flex;
364
+ justify-content: space-between;
365
+ align-items: center;
366
+ }
367
+
368
+ .task-detail {
369
+ font-size: 0.8rem;
370
+ color: #64748b;
371
+ margin-bottom: 0.25rem;
372
+ }
373
+
374
+ /* Skill tag badge */
375
+ .skill-tag {
376
+ display: inline-block;
377
+ background: #dbeafe;
378
+ color: #1d4ed8;
379
+ padding: 2px 8px;
380
+ border-radius: 4px;
381
+ font-size: 0.7rem;
382
+ font-weight: 500;
383
+ }
384
+
385
+ .assigned-badge {
386
+ color: #10b981;
387
+ font-weight: 500;
388
+ }
389
+
390
+ .unassigned-badge {
391
+ color: #9ca3af;
392
+ font-style: italic;
393
+ }
394
+
395
+ /* =============================================================================
396
+ * 8. RESOURCE CARDS
397
+ *
398
+ * Visualization for problem facts (resources that can be assigned to).
399
+ *
400
+ * FEATURES:
401
+ * - Blue gradient background
402
+ * - Capacity bar showing utilization
403
+ * - Skills list
404
+ *
405
+ * CUSTOMIZATION:
406
+ * Modify renderResources() in app.js to change the display.
407
+ * ============================================================================= */
408
+ .resource-card {
409
+ background: linear-gradient(135deg, #eff6ff 0%, #dbeafe 100%);
410
+ border-radius: 10px;
411
+ padding: 1rem;
412
+ margin-bottom: 0.75rem;
413
+ border: 1px solid #bfdbfe;
414
+ }
415
+
416
+ .resource-header {
417
+ display: flex;
418
+ justify-content: space-between;
419
+ align-items: center;
420
+ margin-bottom: 0.5rem;
421
+ }
422
+
423
+ .resource-name {
424
+ font-weight: 600;
425
+ color: #1e40af;
426
+ }
427
+
428
+ .resource-stats {
429
+ font-size: 0.75rem;
430
+ color: #3b82f6;
431
+ }
432
+
433
+ /* Capacity utilization bar */
434
+ .capacity-bar {
435
+ height: 6px;
436
+ background: #e2e8f0;
437
+ border-radius: 3px;
438
+ overflow: hidden;
439
+ margin-top: 0.5rem;
440
+ }
441
+
442
+ .capacity-fill {
443
+ height: 100%;
444
+ background: #10b981; /* Green = under capacity */
445
+ border-radius: 3px;
446
+ transition: width 0.3s ease, background 0.3s ease;
447
+ }
448
+
449
+ /* Warning/danger states for capacity */
450
+ .capacity-fill.warning { background: #f59e0b; } /* 80-100% */
451
+ .capacity-fill.danger { background: #ef4444; } /* >100% overflow */
452
+
453
+ .skills-list {
454
+ margin-top: 0.5rem;
455
+ }
456
+
457
+ /* =============================================================================
458
+ * 9. CONSTRAINT LEGEND
459
+ *
460
+ * Clickable badges showing active constraints from constraints.py.
461
+ *
462
+ * CONSTRAINT TYPES:
463
+ * - Hard (red, lock icon): Must be satisfied, violations = infeasible
464
+ * - Soft (green, feather icon): Optimize these, more = worse score
465
+ *
466
+ * Each badge has a data-target attribute pointing to the constraint
467
+ * function in constraints.py. Clicking navigates to Build tab.
468
+ *
469
+ * TODO: Update these badges to match your constraints.py definitions.
470
+ * ============================================================================= */
471
+ .constraints-legend {
472
+ background: white;
473
+ border-radius: 12px;
474
+ padding: 1.25rem;
475
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
476
+ }
477
+
478
+ .constraint-badge {
479
+ display: inline-flex;
480
+ align-items: center;
481
+ gap: 6px;
482
+ padding: 8px 14px;
483
+ border-radius: 20px;
484
+ font-size: 0.85rem;
485
+ cursor: pointer;
486
+ transition: all 0.2s ease;
487
+ }
488
+
489
+ /* Hard constraint styling - red with lock icon */
490
+ .constraint-badge.hard {
491
+ background: #fef2f2;
492
+ color: #dc2626;
493
+ border: 1px solid #fecaca;
494
+ }
495
+
496
+ .constraint-badge.hard:hover {
497
+ background: #fecaca;
498
+ transform: translateY(-2px);
499
+ }
500
+
501
+ /* Soft constraint styling - green with feather icon */
502
+ .constraint-badge.soft {
503
+ background: #f0fdf4;
504
+ color: #16a34a;
505
+ border: 1px solid #bbf7d0;
506
+ }
507
+
508
+ .constraint-badge.soft:hover {
509
+ background: #bbf7d0;
510
+ transform: translateY(-2px);
511
+ }
512
+
513
+ /* =============================================================================
514
+ * 10. BUILD TAB STYLES
515
+ *
516
+ * The Build tab is a source code viewer showing actual quickstart files.
517
+ *
518
+ * COMPONENTS:
519
+ * - File navigator: Tree view of project files
520
+ * - Code viewer: Syntax-highlighted code display
521
+ * - Annotations: Jump-to-section links
522
+ *
523
+ * HOW IT WORKS:
524
+ * 1. User clicks a file in the navigator
525
+ * 2. app.js loads the source code (embedded or fetched)
526
+ * 3. Prism.js highlights the syntax
527
+ * 4. Annotations show key sections to jump to
528
+ * ============================================================================= */
529
+
530
+ /* File navigator sidebar */
531
+ .file-nav {
532
+ background: white;
533
+ border-radius: 12px;
534
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
535
+ overflow: hidden;
536
+ }
537
+
538
+ .file-nav-header {
539
+ background: #f8fafc;
540
+ padding: 1rem;
541
+ border-bottom: 1px solid #e2e8f0;
542
+ font-weight: 600;
543
+ color: #334155;
544
+ }
545
+
546
+ .file-tree {
547
+ padding: 0.5rem;
548
+ }
549
+
550
+ .folder-item {
551
+ padding: 0.5rem;
552
+ }
553
+
554
+ .folder-name {
555
+ color: #64748b;
556
+ font-size: 0.85rem;
557
+ font-weight: 500;
558
+ margin-bottom: 0.25rem;
559
+ }
560
+
561
+ /* File item in the tree */
562
+ .file-item {
563
+ padding: 0.5rem 0.75rem;
564
+ margin-left: 1rem;
565
+ border-radius: 6px;
566
+ cursor: pointer;
567
+ display: flex;
568
+ align-items: center;
569
+ gap: 8px;
570
+ font-size: 0.85rem;
571
+ color: #475569;
572
+ transition: all 0.2s ease;
573
+ }
574
+
575
+ .file-item:hover {
576
+ background: #f1f5f9;
577
+ }
578
+
579
+ .file-item.active {
580
+ background: #dbeafe;
581
+ color: #1d4ed8;
582
+ }
583
+
584
+ .file-item i {
585
+ width: 16px;
586
+ }
587
+
588
+ /* File type badge */
589
+ .file-badge {
590
+ margin-left: auto;
591
+ font-size: 0.65rem;
592
+ background: #f1f5f9;
593
+ padding: 2px 6px;
594
+ border-radius: 4px;
595
+ color: #64748b;
596
+ }
597
+
598
+ /* Main code viewer panel */
599
+ .code-viewer {
600
+ background: #0f172a;
601
+ border-radius: 12px;
602
+ overflow: hidden;
603
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
604
+ }
605
+
606
+ .code-viewer-header {
607
+ background: #1e293b;
608
+ padding: 0.75rem 1rem;
609
+ display: flex;
610
+ justify-content: space-between;
611
+ align-items: center;
612
+ border-bottom: 1px solid #334155;
613
+ }
614
+
615
+ .code-path {
616
+ color: #94a3b8;
617
+ font-family: monospace;
618
+ font-size: 0.85rem;
619
+ }
620
+
621
+ .code-viewer-body {
622
+ max-height: 600px;
623
+ overflow-y: auto;
624
+ }
625
+
626
+ .code-viewer-body pre {
627
+ margin: 0 !important;
628
+ padding: 1rem !important;
629
+ background: transparent !important;
630
+ }
631
+
632
+ /* Code annotations sidebar */
633
+ .code-annotations {
634
+ background: white;
635
+ border-radius: 12px;
636
+ padding: 1rem;
637
+ margin-top: 1rem;
638
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
639
+ }
640
+
641
+ .annotation-item {
642
+ padding: 0.5rem 0.75rem;
643
+ border-radius: 6px;
644
+ cursor: pointer;
645
+ display: flex;
646
+ justify-content: space-between;
647
+ align-items: center;
648
+ font-size: 0.85rem;
649
+ transition: background 0.2s;
650
+ }
651
+
652
+ .annotation-item:hover {
653
+ background: #f1f5f9;
654
+ }
655
+
656
+ .annotation-line {
657
+ color: #94a3b8;
658
+ font-size: 0.75rem;
659
+ }
660
+
661
+ /* =============================================================================
662
+ * 11. NAVIGATION OVERRIDES
663
+ *
664
+ * Custom styling for the main tab navigation.
665
+ * Uses pill-style buttons with rounded corners.
666
+ * ============================================================================= */
667
+ .nav-main {
668
+ display: flex;
669
+ gap: 0.5rem;
670
+ }
671
+
672
+ .nav-main .nav-link {
673
+ border-radius: 8px;
674
+ padding: 0.5rem 1rem;
675
+ color: #475569;
676
+ font-weight: 500;
677
+ transition: all 0.2s ease;
678
+ }
679
+
680
+ .nav-main .nav-link:hover {
681
+ background: #f1f5f9;
682
+ color: #1e293b;
683
+ }
684
+
685
+ /* Active tab styling - green background */
686
+ .nav-main .nav-link.active {
687
+ background: #10b981;
688
+ color: white;
689
+ }
690
+
691
+ .nav-main .nav-link i {
692
+ margin-right: 6px;
693
+ }
694
+
695
+ /* =============================================================================
696
+ * 12. NOTIFICATION PANEL
697
+ *
698
+ * Fixed position panel for showing notifications (errors, success messages).
699
+ * Positioned in top-right corner.
700
+ * ============================================================================= */
701
+ #notificationPanel {
702
+ position: fixed;
703
+ top: 1rem;
704
+ right: 1rem;
705
+ z-index: 1050;
706
+ }
707
+
708
+ /* =============================================================================
709
+ * 13. RESPONSIVE ADJUSTMENTS
710
+ *
711
+ * Mobile-friendly modifications for smaller screens.
712
+ * ============================================================================= */
713
+ @media (max-width: 768px) {
714
+ /* Single column task grid on mobile */
715
+ .task-grid {
716
+ grid-template-columns: 1fr;
717
+ }
718
+
719
+ /* Smaller KPI values */
720
+ .kpi-value {
721
+ font-size: 1.5rem;
722
+ }
723
+
724
+ /* Hide progress step labels on mobile */
725
+ .step-label {
726
+ display: none;
727
+ }
728
+ }
static/app.js ADDED
@@ -0,0 +1,1836 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * =============================================================================
3
+ * SOLVERFORGE QUICKSTART TEMPLATE - APPLICATION JAVASCRIPT
4
+ * =============================================================================
5
+ *
6
+ * This file contains all the client-side logic for the SolverForge quickstart
7
+ * template. It implements a "code-link" educational UI that teaches users
8
+ * how to build the very interface they're looking at.
9
+ *
10
+ * FILE STRUCTURE:
11
+ * ---------------
12
+ * 1. GLOBAL STATE - Variables tracking UI state, loaded data, solving jobs
13
+ * 2. INITIALIZATION - Document ready handler and app setup
14
+ * 3. AJAX CONFIGURATION - jQuery AJAX setup and HTTP method extensions
15
+ * 4. DEMO DATA LOADING - Fetching and selecting sample datasets
16
+ * 5. SCHEDULE/SOLUTION LOADING - Getting solution data from backend
17
+ * 6. RENDERING - Card-based visualization of tasks and resources
18
+ * 7. KPI UPDATES - Key Performance Indicator card updates
19
+ * 8. SOLVING OPERATIONS - Start, stop, and poll the solver
20
+ * 9. SCORE ANALYSIS - Constraint breakdown modal
21
+ * 10. TAB NAVIGATION HELPERS - Programmatic tab switching
22
+ * 11. BUILD TAB - Source code viewer with syntax highlighting
23
+ * 12. INTERACTIVE CODE FEATURES - Click-to-code navigation
24
+ * 13. NOTIFICATIONS - Toast messages for errors and info
25
+ * 14. UTILITY FUNCTIONS - Helpers and formatters
26
+ * 15. RESOURCE & TASK CRUD - Adding and removing entities dynamically
27
+ * 16. CONSTRAINT WEIGHT CONTROLS - Adjusting optimization weights
28
+ *
29
+ * CUSTOMIZATION GUIDE:
30
+ * --------------------
31
+ * When adapting this template for your domain:
32
+ *
33
+ * 1. renderSolution() - Change task card layout for your entities
34
+ * 2. renderResources() - Change resource card layout for your facts
35
+ * 3. updateKPIs() - Update metrics shown in KPI cards
36
+ * 4. countViolations() - Implement violation detection for your constraints
37
+ *
38
+ * API ENDPOINTS USED:
39
+ * -------------------
40
+ * - GET /demo-data - List available demo datasets
41
+ * - GET /demo-data/{id} - Get a specific demo dataset
42
+ * - POST /schedules - Start solving (returns job ID)
43
+ * - GET /schedules/{jobId} - Get current solution
44
+ * - DELETE /schedules/{jobId}- Stop solving
45
+ * - PUT /schedules/analyze - Analyze score breakdown
46
+ */
47
+
48
+
49
+ // =============================================================================
50
+ // 1. GLOBAL STATE
51
+ // =============================================================================
52
+ // These variables track the application state throughout the session.
53
+ // They are modified by various functions and checked to determine UI behavior.
54
+
55
+ /**
56
+ * Interval ID for auto-refreshing the solution while solving.
57
+ * Set by setInterval() when solving starts, cleared when solving stops.
58
+ * Used to poll the backend for updates every 2 seconds.
59
+ *
60
+ * @type {number|null}
61
+ */
62
+ let autoRefreshIntervalId = null;
63
+
64
+ /**
65
+ * Currently selected demo data ID (e.g., "SMALL", "MEDIUM", "LARGE").
66
+ * Set when user selects from the Data dropdown.
67
+ * Used to fetch the initial dataset before solving.
68
+ *
69
+ * @type {string|null}
70
+ */
71
+ let demoDataId = null;
72
+
73
+ /**
74
+ * Current solving job ID (UUID string from the backend).
75
+ * Set when solve() successfully starts a job.
76
+ * Used to poll for updates and stop solving.
77
+ *
78
+ * @type {string|null}
79
+ */
80
+ let scheduleId = null;
81
+
82
+ /**
83
+ * The currently loaded schedule/solution data.
84
+ * Contains the full problem definition and current solution:
85
+ * - resources: Array of resource objects (problem facts)
86
+ * - tasks: Array of task objects (planning entities)
87
+ * - score: HardSoftScore string (e.g., "0hard/-50soft")
88
+ * - solverStatus: "NOT_SOLVING" or "SOLVING"
89
+ *
90
+ * @type {Object|null}
91
+ */
92
+ let loadedSchedule = null;
93
+
94
+ /**
95
+ * Currently displayed file in the Build tab code viewer.
96
+ * Used to track which file is being shown and for copy functionality.
97
+ *
98
+ * @type {string}
99
+ */
100
+ let currentFile = 'domain.py';
101
+
102
+ /**
103
+ * Cached source code content for the current file.
104
+ * Populated by loadSourceFile() when fetching from API.
105
+ *
106
+ * @type {string}
107
+ */
108
+ let currentFileContent = '';
109
+
110
+
111
+ // =============================================================================
112
+ // 2. INITIALIZATION
113
+ // =============================================================================
114
+ // Application startup code. Sets up event handlers and loads initial data.
115
+
116
+ /**
117
+ * Document ready handler with safe initialization.
118
+ *
119
+ * PATTERN: Double-initialization
120
+ * We use both $(window).on('load') and setTimeout() to ensure initialization
121
+ * happens even if some external resources load slowly or fail to fire the
122
+ * load event.
123
+ *
124
+ * This pattern is common in SolverForge quickstarts to handle:
125
+ * - Slow CDN responses
126
+ * - Browser caching issues
127
+ * - Race conditions with external scripts
128
+ */
129
+ $(document).ready(function () {
130
+ let initialized = false;
131
+
132
+ /**
133
+ * Safe initialization wrapper.
134
+ * Ensures initializeApp() is only called once.
135
+ */
136
+ function safeInitialize() {
137
+ if (!initialized) {
138
+ initialized = true;
139
+ initializeApp();
140
+ }
141
+ }
142
+
143
+ // Primary: Initialize when all resources (images, scripts) are loaded
144
+ $(window).on('load', safeInitialize);
145
+
146
+ // Fallback: Initialize after short delay if load event doesn't fire
147
+ setTimeout(safeInitialize, 100);
148
+ });
149
+
150
+ /**
151
+ * Main initialization function.
152
+ *
153
+ * Called once when the page is ready. This function:
154
+ * 1. Sets up button click handlers
155
+ * 2. Configures AJAX defaults
156
+ * 3. Loads the demo data list
157
+ * 4. Initializes the Build tab code viewer
158
+ * 5. Sets up code-link click handlers
159
+ *
160
+ * CUSTOMIZATION: Add your own initialization code here.
161
+ */
162
+ function initializeApp() {
163
+ console.log('SolverForge Quickstart Template initializing...');
164
+
165
+ // =========================================================================
166
+ // BUTTON CLICK HANDLERS
167
+ // =========================================================================
168
+
169
+ // Solve button - starts the optimization
170
+ // Connected to solve() function which POSTs to /schedules
171
+ $("#solveButton").click(function () {
172
+ solve();
173
+ });
174
+
175
+ // Stop button - terminates solving early
176
+ // Connected to stopSolving() which DELETEs /schedules/{id}
177
+ $("#stopSolvingButton").click(function () {
178
+ stopSolving();
179
+ });
180
+
181
+ // Analyze button - shows score breakdown modal
182
+ // Connected to analyze() which PUTs to /schedules/analyze
183
+ $("#analyzeButton").click(function () {
184
+ analyze();
185
+ });
186
+
187
+ // =========================================================================
188
+ // AJAX SETUP & DATA LOADING
189
+ // =========================================================================
190
+
191
+ // Configure jQuery AJAX defaults (headers, methods)
192
+ setupAjax();
193
+
194
+ // Load the list of available demo datasets
195
+ fetchDemoData();
196
+
197
+ // =========================================================================
198
+ // BUILD TAB INITIALIZATION
199
+ // =========================================================================
200
+
201
+ // Set up file navigator click handlers
202
+ setupBuildTab();
203
+
204
+ // Load the default file (domain.py)
205
+ loadSourceFile('domain.py');
206
+
207
+ // =========================================================================
208
+ // INTERACTIVE CODE FEATURE INITIALIZATION
209
+ // =========================================================================
210
+
211
+ // Set up click handlers for code-link elements
212
+ setupCodeLinkHandlers();
213
+
214
+ console.log('Initialization complete');
215
+ }
216
+
217
+
218
+ // =============================================================================
219
+ // 3. AJAX CONFIGURATION
220
+ // =============================================================================
221
+ // jQuery AJAX setup for communicating with the backend REST API.
222
+
223
+ /**
224
+ * Configures jQuery AJAX with proper headers and HTTP method extensions.
225
+ *
226
+ * WHAT THIS DOES:
227
+ * 1. Sets default Content-Type and Accept headers for JSON
228
+ * 2. Adds $.put() and $.delete() methods to jQuery
229
+ * (jQuery only has $.get() and $.post() by default)
230
+ *
231
+ * WHY WE NEED THIS:
232
+ * RESTful APIs use all HTTP methods (GET, POST, PUT, DELETE).
233
+ * The Accept header includes text/plain because job IDs are returned as text.
234
+ */
235
+ function setupAjax() {
236
+ // Set default headers for all AJAX requests
237
+ $.ajaxSetup({
238
+ headers: {
239
+ 'Content-Type': 'application/json',
240
+ 'Accept': 'application/json,text/plain', // text/plain for job ID
241
+ }
242
+ });
243
+
244
+ // Extend jQuery with PUT and DELETE methods
245
+ // These mirror the signature of $.get() and $.post()
246
+ jQuery.each(["put", "delete"], function (i, method) {
247
+ jQuery[method] = function (url, data, callback, type) {
248
+ // Handle optional parameters (data can be omitted)
249
+ if (jQuery.isFunction(data)) {
250
+ type = type || callback;
251
+ callback = data;
252
+ data = undefined;
253
+ }
254
+ return jQuery.ajax({
255
+ url: url,
256
+ type: method,
257
+ dataType: type,
258
+ data: data,
259
+ success: callback
260
+ });
261
+ };
262
+ });
263
+ }
264
+
265
+
266
+ // =============================================================================
267
+ // 4. DEMO DATA LOADING
268
+ // =============================================================================
269
+ // Functions for loading sample datasets from the backend.
270
+
271
+ /**
272
+ * Fetches the list of available demo datasets and populates the dropdown.
273
+ *
274
+ * FLOW:
275
+ * 1. GET /demo-data returns ["SMALL", "MEDIUM", "LARGE"] (or similar)
276
+ * 2. For each dataset, create a dropdown menu item
277
+ * 3. Auto-select and load the first dataset
278
+ *
279
+ * CUSTOMIZATION:
280
+ * The backend demo_data.py defines what datasets are available.
281
+ * Each dataset is a complete Schedule object with resources and tasks.
282
+ */
283
+ function fetchDemoData() {
284
+ $.get("/demo-data", function (data) {
285
+ const dropdown = $("#dataDropdown");
286
+ dropdown.empty();
287
+
288
+ // Create a dropdown item for each available dataset
289
+ data.forEach(item => {
290
+ const menuItem = $(`
291
+ <li>
292
+ <a class="dropdown-item" href="#" data-dataset="${item}">
293
+ ${item}
294
+ </a>
295
+ </li>
296
+ `);
297
+
298
+ // Click handler for this dataset
299
+ menuItem.find('a').click(function (e) {
300
+ e.preventDefault();
301
+
302
+ // Update visual selection
303
+ dropdown.find('.dropdown-item').removeClass('active');
304
+ $(this).addClass('active');
305
+
306
+ // Reset solving state and load new data
307
+ scheduleId = null;
308
+ demoDataId = item;
309
+
310
+ // Load and display the selected dataset
311
+ refreshSchedule();
312
+ });
313
+
314
+ dropdown.append(menuItem);
315
+ });
316
+
317
+ // Auto-select the first dataset
318
+ if (data.length > 0) {
319
+ demoDataId = data[0];
320
+ dropdown.find('.dropdown-item').first().addClass('active');
321
+ refreshSchedule();
322
+ }
323
+ }).fail(function (xhr, ajaxOptions, thrownError) {
324
+ // Handle case where backend is not running or has no data
325
+ showNotification("Failed to load demo data. Is the server running?", "danger");
326
+ console.error('Failed to fetch demo data:', thrownError);
327
+ });
328
+ }
329
+
330
+
331
+ // =============================================================================
332
+ // 5. SCHEDULE/SOLUTION LOADING
333
+ // =============================================================================
334
+ // Functions for fetching and displaying solution data.
335
+
336
+ /**
337
+ * Fetches and displays the current schedule/solution.
338
+ *
339
+ * LOGIC:
340
+ * - If scheduleId is set: GET /schedules/{scheduleId} for solving progress
341
+ * - If scheduleId is null: GET /demo-data/{demoDataId} for initial data
342
+ *
343
+ * WHEN CALLED:
344
+ * - When a dataset is selected from the dropdown
345
+ * - Every 2 seconds while solving (via setInterval)
346
+ * - After stopping solving
347
+ */
348
+ function refreshSchedule() {
349
+ // Determine which endpoint to call
350
+ let path = "/schedules/" + scheduleId;
351
+ if (scheduleId === null) {
352
+ // No active job - load demo data instead
353
+ if (demoDataId === null) {
354
+ showNotification("Please select a dataset from the Data dropdown.", "warning");
355
+ return;
356
+ }
357
+ path = "/demo-data/" + demoDataId;
358
+ }
359
+
360
+ // Fetch the schedule data
361
+ $.getJSON(path, function (schedule) {
362
+ loadedSchedule = schedule;
363
+ renderSchedule(schedule);
364
+ }).fail(function (xhr, ajaxOptions, thrownError) {
365
+ showNotification("Failed to load schedule data.", "danger");
366
+ console.error('Failed to fetch schedule:', thrownError);
367
+ refreshSolvingButtons(false);
368
+ });
369
+ }
370
+
371
+ /**
372
+ * Renders the complete schedule/solution to the UI.
373
+ *
374
+ * UPDATES:
375
+ * - Solve/Stop button visibility
376
+ * - Spinner animation
377
+ * - KPI cards
378
+ * - Task cards in the tasks panel
379
+ * - Resource cards in the resources panel
380
+ *
381
+ * @param {Object} schedule - The schedule data from the backend
382
+ */
383
+ function renderSchedule(schedule) {
384
+ if (!schedule) {
385
+ console.error('No schedule data provided to renderSchedule');
386
+ return;
387
+ }
388
+
389
+ console.log('Rendering schedule:', schedule);
390
+
391
+ // Update solving buttons based on solver status
392
+ const isSolving = schedule.solverStatus != null &&
393
+ schedule.solverStatus !== "NOT_SOLVING";
394
+ refreshSolvingButtons(isSolving);
395
+
396
+ // Update KPI cards with current metrics
397
+ updateKPIs(schedule);
398
+
399
+ // Render the solution visualization (task cards)
400
+ renderSolution(schedule);
401
+
402
+ // Render the resources panel
403
+ renderResources(schedule);
404
+ }
405
+
406
+
407
+ // =============================================================================
408
+ // 6. RENDERING - Card-Based Visualization
409
+ // =============================================================================
410
+ // Functions that create the visual representation of tasks and resources.
411
+
412
+ /**
413
+ * Renders the tasks panel with card-based layout.
414
+ *
415
+ * CARD STATES:
416
+ * - Default (green border): Task is assigned to a resource
417
+ * - .unassigned (orange border): Task has no resource assigned
418
+ * - .violation (red border): Task has a constraint violation
419
+ *
420
+ * CUSTOMIZATION:
421
+ * Modify this function to match your domain model:
422
+ * - Change what fields are displayed
423
+ * - Add domain-specific badges or indicators
424
+ * - Implement custom violation detection
425
+ *
426
+ * @param {Object} schedule - Schedule containing tasks array
427
+ */
428
+ function renderSolution(schedule) {
429
+ const panel = $("#tasksPanel");
430
+ panel.empty();
431
+
432
+ // Update task count badge
433
+ const taskCount = schedule.tasks ? schedule.tasks.length : 0;
434
+ $("#taskCount").text(taskCount);
435
+
436
+ // Handle empty state
437
+ if (!schedule.tasks || schedule.tasks.length === 0) {
438
+ panel.html('<p class="text-muted text-center">No tasks in this dataset</p>');
439
+ return;
440
+ }
441
+
442
+ // Create the task grid container
443
+ const grid = $('<div class="task-grid"></div>');
444
+
445
+ // Render each task as a card
446
+ schedule.tasks.forEach(task => {
447
+ const card = createTaskCard(task, schedule);
448
+ grid.append(card);
449
+ });
450
+
451
+ panel.append(grid);
452
+ }
453
+
454
+ /**
455
+ * Creates a single task card element.
456
+ *
457
+ * STRUCTURE:
458
+ * <div class="task-card [unassigned|violation] code-link">
459
+ * <div class="task-name">Task Name <duration></div>
460
+ * <div class="task-detail">Skill: skill_name</div>
461
+ * <div class="task-detail">Assigned: resource_name</div>
462
+ * </div>
463
+ *
464
+ * CUSTOMIZATION:
465
+ * Modify this to show your domain-specific fields.
466
+ *
467
+ * @param {Object} task - The task object
468
+ * @param {Object} schedule - The full schedule (for violation checking)
469
+ * @returns {jQuery} The task card jQuery element
470
+ */
471
+ function createTaskCard(task, schedule) {
472
+ // Determine card state
473
+ const isAssigned = task.resource != null;
474
+ const hasViolation = checkTaskViolation(task, schedule);
475
+
476
+ // Build CSS classes
477
+ let cardClass = 'task-card code-link';
478
+ if (hasViolation) {
479
+ cardClass += ' violation';
480
+ } else if (!isAssigned) {
481
+ cardClass += ' unassigned';
482
+ }
483
+
484
+ // Create the card (escaping id for onclick)
485
+ const escapedId = task.id.replace(/'/g, "\\'");
486
+ const card = $(`<div class="${cardClass}" data-target="app.js:createTaskCard"></div>`);
487
+
488
+ // Task name, duration, and remove button
489
+ const nameRow = $('<div class="task-name"></div>');
490
+ nameRow.append($('<span></span>').text(task.name));
491
+ const rightSide = $('<div class="d-flex align-items-center gap-2"></div>');
492
+ rightSide.append($('<span class="task-duration"></span>').text(`${task.duration}m`));
493
+ rightSide.append($(`<button class="btn btn-sm btn-outline-danger" onclick="removeTask('${escapedId}', event)" title="Remove Task"><i class="fas fa-minus"></i></button>`));
494
+ nameRow.append(rightSide);
495
+ card.append(nameRow);
496
+
497
+ // Required skill (if any)
498
+ if (task.requiredSkill) {
499
+ const skillRow = $('<div class="task-detail"></div>');
500
+ skillRow.append($('<span class="skill-tag"></span>').text(task.requiredSkill));
501
+ card.append(skillRow);
502
+ }
503
+
504
+ // Assignment status
505
+ const assignmentRow = $('<div class="task-detail"></div>');
506
+ if (isAssigned) {
507
+ assignmentRow.html(`<span class="assigned-badge"><i class="fas fa-check me-1"></i>${task.resource}</span>`);
508
+ } else {
509
+ assignmentRow.html('<span class="unassigned-badge">Unassigned</span>');
510
+ }
511
+ card.append(assignmentRow);
512
+
513
+ return card;
514
+ }
515
+
516
+ /**
517
+ * Checks if a task has any constraint violations.
518
+ *
519
+ * CUSTOMIZATION:
520
+ * Implement your domain-specific violation detection here.
521
+ * This example checks:
522
+ * - Required skill: Is the task assigned to a resource with the required skill?
523
+ *
524
+ * @param {Object} task - The task to check
525
+ * @param {Object} schedule - The schedule containing resources
526
+ * @returns {boolean} True if task has a violation
527
+ */
528
+ function checkTaskViolation(task, schedule) {
529
+ // If not assigned, it's not a violation (just unassigned)
530
+ if (!task.resource) {
531
+ return false;
532
+ }
533
+
534
+ // Check required skill constraint
535
+ if (task.requiredSkill) {
536
+ const resource = schedule.resources.find(r => r.name === task.resource);
537
+ if (resource) {
538
+ // Check if resource has the required skill
539
+ const hasSkill = resource.skills &&
540
+ resource.skills.includes(task.requiredSkill);
541
+ if (!hasSkill) {
542
+ return true; // Skill violation!
543
+ }
544
+ }
545
+ }
546
+
547
+ return false;
548
+ }
549
+
550
+ /**
551
+ * Renders the resources panel with card-based layout.
552
+ *
553
+ * CARD STRUCTURE:
554
+ * - Resource name
555
+ * - Capacity utilization bar (color-coded)
556
+ * - Skills list
557
+ *
558
+ * CUSTOMIZATION:
559
+ * Modify this function to match your problem facts.
560
+ *
561
+ * @param {Object} schedule - Schedule containing resources array
562
+ */
563
+ function renderResources(schedule) {
564
+ const panel = $("#resourcesPanel");
565
+ panel.empty();
566
+
567
+ // Update resource count badge
568
+ const resourceCount = schedule.resources ? schedule.resources.length : 0;
569
+ $("#resourceCount").text(resourceCount);
570
+
571
+ // Handle empty state
572
+ if (!schedule.resources || schedule.resources.length === 0) {
573
+ panel.html('<p class="text-muted text-center">No resources in this dataset</p>');
574
+ return;
575
+ }
576
+
577
+ // Render each resource as a card
578
+ schedule.resources.forEach(resource => {
579
+ const card = createResourceCard(resource, schedule);
580
+ panel.append(card);
581
+ });
582
+ }
583
+
584
+ /**
585
+ * Creates a single resource card element.
586
+ *
587
+ * FEATURES:
588
+ * - Capacity bar showing utilization
589
+ * - Color-coded: green (<80%), orange (80-100%), red (>100%)
590
+ * - Skills displayed as tags
591
+ *
592
+ * @param {Object} resource - The resource object
593
+ * @param {Object} schedule - The schedule (for calculating utilization)
594
+ * @returns {jQuery} The resource card jQuery element
595
+ */
596
+ function createResourceCard(resource, schedule) {
597
+ // Calculate utilization
598
+ const totalDuration = schedule.tasks
599
+ ? schedule.tasks
600
+ .filter(t => t.resource === resource.name)
601
+ .reduce((sum, t) => sum + t.duration, 0)
602
+ : 0;
603
+ const utilization = resource.capacity > 0
604
+ ? (totalDuration / resource.capacity) * 100
605
+ : 0;
606
+
607
+ // Determine capacity bar color
608
+ let fillClass = '';
609
+ if (utilization > 100) {
610
+ fillClass = 'danger';
611
+ } else if (utilization > 80) {
612
+ fillClass = 'warning';
613
+ }
614
+
615
+ // Create skills badges HTML
616
+ const skillsHtml = resource.skills && resource.skills.length > 0
617
+ ? resource.skills.map(s => `<span class="skill-tag me-1">${s}</span>`).join('')
618
+ : '<span class="text-muted small">No skills</span>';
619
+
620
+ // Build the card (escaping name for onclick)
621
+ const escapedName = resource.name.replace(/'/g, "\\'");
622
+ const card = $(`
623
+ <div class="resource-card code-link" data-target="app.js:createResourceCard">
624
+ <div class="resource-header">
625
+ <span class="resource-name">${resource.name}</span>
626
+ <div class="d-flex align-items-center gap-2">
627
+ <span class="resource-stats">${totalDuration}/${resource.capacity} min</span>
628
+ <button class="btn btn-sm btn-outline-danger" onclick="removeResource('${escapedName}', event)" title="Remove Resource">
629
+ <i class="fas fa-minus"></i>
630
+ </button>
631
+ </div>
632
+ </div>
633
+ <div class="capacity-bar">
634
+ <div class="capacity-fill ${fillClass}" style="width: ${Math.min(utilization, 100)}%"></div>
635
+ </div>
636
+ <div class="skills-list mt-2">
637
+ ${skillsHtml}
638
+ </div>
639
+ </div>
640
+ `);
641
+
642
+ return card;
643
+ }
644
+
645
+
646
+ // =============================================================================
647
+ // 7. KPI UPDATES
648
+ // =============================================================================
649
+ // Functions for updating the Key Performance Indicator cards.
650
+
651
+ /**
652
+ * Updates all KPI cards with current metrics.
653
+ *
654
+ * KPIs DISPLAYED:
655
+ * - Total Tasks: Number of planning entities
656
+ * - Assigned: Tasks with non-null planning variable
657
+ * - Violations: Hard constraint violations
658
+ * - Score: Current HardSoftScore
659
+ *
660
+ * ANIMATION:
661
+ * KPI values pulse when they change (using .kpi-pulse class).
662
+ *
663
+ * CUSTOMIZATION:
664
+ * Modify this to show metrics relevant to your domain.
665
+ *
666
+ * @param {Object} schedule - The schedule data
667
+ */
668
+ function updateKPIs(schedule) {
669
+ // Calculate metrics
670
+ const totalTasks = schedule.tasks ? schedule.tasks.length : 0;
671
+ const assignedTasks = schedule.tasks
672
+ ? schedule.tasks.filter(t => t.resource != null).length
673
+ : 0;
674
+ const violations = countViolations(schedule);
675
+ const score = schedule.score || '?';
676
+
677
+ // Update KPI values with pulse animation
678
+ updateKPIValue('#kpiTotalTasks', totalTasks);
679
+ updateKPIValue('#kpiAssigned', assignedTasks);
680
+ updateKPIValue('#kpiViolations', violations);
681
+ updateKPIValue('#kpiScore', formatScore(score));
682
+ }
683
+
684
+ /**
685
+ * Updates a single KPI value with optional pulse animation.
686
+ *
687
+ * @param {string} selector - jQuery selector for the KPI value element
688
+ * @param {string|number} newValue - The new value to display
689
+ */
690
+ function updateKPIValue(selector, newValue) {
691
+ const el = $(selector);
692
+ const oldValue = el.text();
693
+
694
+ // Only animate if value changed
695
+ if (oldValue !== String(newValue)) {
696
+ el.text(newValue);
697
+ el.addClass('kpi-pulse');
698
+ setTimeout(() => el.removeClass('kpi-pulse'), 500);
699
+ }
700
+ }
701
+
702
+ /**
703
+ * Counts the number of hard constraint violations.
704
+ *
705
+ * CUSTOMIZATION:
706
+ * Implement your domain-specific violation counting here.
707
+ * This example counts:
708
+ * - Required skill violations
709
+ * - Capacity violations
710
+ *
711
+ * @param {Object} schedule - The schedule data
712
+ * @returns {number} Number of violations
713
+ */
714
+ function countViolations(schedule) {
715
+ if (!schedule.tasks || !schedule.resources) {
716
+ return 0;
717
+ }
718
+
719
+ let violations = 0;
720
+
721
+ // Count required skill violations
722
+ schedule.tasks.forEach(task => {
723
+ if (task.resource && task.requiredSkill) {
724
+ const resource = schedule.resources.find(r => r.name === task.resource);
725
+ if (resource && resource.skills) {
726
+ if (!resource.skills.includes(task.requiredSkill)) {
727
+ violations++;
728
+ }
729
+ }
730
+ }
731
+ });
732
+
733
+ // Count capacity violations
734
+ schedule.resources.forEach(resource => {
735
+ const totalDuration = schedule.tasks
736
+ .filter(t => t.resource === resource.name)
737
+ .reduce((sum, t) => sum + t.duration, 0);
738
+ if (totalDuration > resource.capacity) {
739
+ violations++;
740
+ }
741
+ });
742
+
743
+ return violations;
744
+ }
745
+
746
+ /**
747
+ * Formats a score string for display.
748
+ *
749
+ * EXAMPLES:
750
+ * - "0hard/-50soft" -> "0/-50"
751
+ * - "-2hard/-15soft" -> "-2/-15"
752
+ * - null -> "?"
753
+ *
754
+ * @param {string|null} score - The score string
755
+ * @returns {string} Formatted score
756
+ */
757
+ function formatScore(score) {
758
+ if (!score || score === '?') {
759
+ return '?';
760
+ }
761
+
762
+ const components = getScoreComponents(score);
763
+
764
+ // Format as hard/soft
765
+ return `${components.hard}/${components.soft}`;
766
+ }
767
+
768
+
769
+ // =============================================================================
770
+ // 8. SOLVING OPERATIONS
771
+ // =============================================================================
772
+ // Functions for starting, stopping, and monitoring the solver.
773
+
774
+ /**
775
+ * Starts the optimization solver.
776
+ *
777
+ * FLOW:
778
+ * 1. Get current constraint weights from UI sliders
779
+ * 2. POST schedule + weights to /schedules
780
+ * 3. Backend returns a job ID (UUID)
781
+ * 4. Store job ID and start polling for updates
782
+ *
783
+ * POLLING:
784
+ * While solving, refreshSchedule() is called every 2 seconds
785
+ * via setInterval(). This polls GET /schedules/{jobId}.
786
+ */
787
+ function solve() {
788
+ // Check that we have data to solve
789
+ if (!loadedSchedule) {
790
+ showNotification("No data loaded. Please select a dataset first.", "warning");
791
+ return;
792
+ }
793
+
794
+ // Get constraint weights from UI sliders
795
+ const constraintWeights = getConstraintWeights();
796
+ console.log('Constraint weights:', constraintWeights);
797
+
798
+ // Build the request payload with schedule and weights
799
+ const payload = {
800
+ ...loadedSchedule,
801
+ constraintWeights: constraintWeights
802
+ };
803
+
804
+ console.log('Starting solver with payload:', payload);
805
+
806
+ // Send the schedule to the solver
807
+ $.post("/schedules", JSON.stringify(payload), function (data) {
808
+ // Store the job ID for future requests
809
+ scheduleId = data;
810
+ console.log('Solving started, job ID:', scheduleId);
811
+
812
+ // Update UI to show solving state
813
+ refreshSolvingButtons(true);
814
+
815
+ showNotification("Solver started!", "success");
816
+ }).fail(function (xhr, ajaxOptions, thrownError) {
817
+ showNotification("Failed to start solving: " + thrownError, "danger");
818
+ console.error('Failed to start solving:', xhr.responseText);
819
+ refreshSolvingButtons(false);
820
+ }, "text");
821
+ }
822
+
823
+ /**
824
+ * Stops the currently running solver.
825
+ *
826
+ * FLOW:
827
+ * 1. DELETE /schedules/{jobId}
828
+ * 2. Backend terminates the solver
829
+ * 3. Update UI to idle state
830
+ * 4. Refresh to show final solution
831
+ */
832
+ function stopSolving() {
833
+ if (!scheduleId) {
834
+ console.warn('No active solving job to stop');
835
+ return;
836
+ }
837
+
838
+ console.log('Stopping solver, job ID:', scheduleId);
839
+
840
+ $.delete(`/schedules/${scheduleId}`, function () {
841
+ // Update UI to show stopped state
842
+ refreshSolvingButtons(false);
843
+
844
+ // Refresh to get final solution
845
+ refreshSchedule();
846
+
847
+ showNotification("Solver stopped", "info");
848
+ }).fail(function (xhr, ajaxOptions, thrownError) {
849
+ showNotification("Failed to stop solving: " + thrownError, "danger");
850
+ console.error('Failed to stop solving:', xhr.responseText);
851
+ });
852
+ }
853
+
854
+ /**
855
+ * Updates the UI to reflect solving/not-solving state.
856
+ *
857
+ * WHEN SOLVING:
858
+ * - Hides Solve button, shows Stop button
859
+ * - Shows spinner animation
860
+ * - Starts polling for updates every 2 seconds
861
+ *
862
+ * WHEN NOT SOLVING:
863
+ * - Shows Solve button, hides Stop button
864
+ * - Hides spinner
865
+ * - Stops polling
866
+ *
867
+ * @param {boolean} solving - Whether solving is currently in progress
868
+ */
869
+ function refreshSolvingButtons(solving) {
870
+ if (solving) {
871
+ // Solving state
872
+ $("#solveButton").hide();
873
+ $("#stopSolvingButton").show();
874
+ $("#solvingSpinner").addClass("active");
875
+
876
+ // Start polling for updates if not already polling
877
+ if (autoRefreshIntervalId == null) {
878
+ autoRefreshIntervalId = setInterval(refreshSchedule, 2000);
879
+ }
880
+ } else {
881
+ // Idle state
882
+ $("#solveButton").show();
883
+ $("#stopSolvingButton").hide();
884
+ $("#solvingSpinner").removeClass("active");
885
+
886
+ // Stop polling
887
+ if (autoRefreshIntervalId != null) {
888
+ clearInterval(autoRefreshIntervalId);
889
+ autoRefreshIntervalId = null;
890
+ }
891
+ }
892
+ }
893
+
894
+
895
+ // =============================================================================
896
+ // 9. SCORE ANALYSIS
897
+ // =============================================================================
898
+ // Functions for displaying the score analysis modal.
899
+
900
+ /**
901
+ * Shows the score analysis modal with constraint breakdown.
902
+ *
903
+ * FLOW:
904
+ * 1. Show the modal
905
+ * 2. PUT /schedules/analyze with current schedule
906
+ * 3. Render constraint breakdown table
907
+ *
908
+ * DISPLAY:
909
+ * - Warning icon for violated hard constraints
910
+ * - Check icon for satisfied constraints
911
+ * - Match count and score contribution
912
+ */
913
+ function analyze() {
914
+ // Show the modal
915
+ const modal = new bootstrap.Modal("#scoreAnalysisModal");
916
+ modal.show();
917
+
918
+ const modalContent = $("#scoreAnalysisContent");
919
+ modalContent.html('<p class="text-center"><i class="fas fa-spinner fa-spin me-2"></i>Analyzing...</p>');
920
+
921
+ // Check if we have a score to analyze
922
+ if (!loadedSchedule) {
923
+ modalContent.html('<p class="text-muted text-center">No data loaded.</p>');
924
+ return;
925
+ }
926
+
927
+ // Update the score label in the modal header
928
+ $('#scoreAnalysisScore').text(loadedSchedule.score || '?');
929
+
930
+ // Fetch the score analysis from the backend
931
+ $.put("/schedules/analyze", JSON.stringify(loadedSchedule), function (scoreAnalysis) {
932
+ renderScoreAnalysis(scoreAnalysis, modalContent);
933
+ }).fail(function (xhr, ajaxOptions, thrownError) {
934
+ modalContent.html('<p class="text-danger text-center">Failed to analyze score.</p>');
935
+ console.error('Failed to analyze score:', xhr.responseText);
936
+ }, "json");
937
+ }
938
+
939
+ /**
940
+ * Renders the score analysis table in the modal.
941
+ *
942
+ * TABLE COLUMNS:
943
+ * - Icon: Warning/check status
944
+ * - Constraint: Name of the constraint
945
+ * - Type: hard/soft
946
+ * - Matches: Number of violations
947
+ * - Weight: Constraint weight
948
+ * - Score: Score contribution
949
+ *
950
+ * @param {Object} scoreAnalysis - The analysis data from the backend
951
+ * @param {jQuery} container - The container element to render into
952
+ */
953
+ function renderScoreAnalysis(scoreAnalysis, container) {
954
+ container.empty();
955
+
956
+ let constraints = scoreAnalysis.constraints || [];
957
+
958
+ if (constraints.length === 0) {
959
+ container.html('<p class="text-muted text-center">No constraint data available.</p>');
960
+ return;
961
+ }
962
+
963
+ // Sort constraints: violated hard constraints first, then by impact
964
+ constraints.sort((a, b) => {
965
+ let aComponents = getScoreComponents(a.score);
966
+ let bComponents = getScoreComponents(b.score);
967
+
968
+ // Hard constraints with negative score first
969
+ if (aComponents.hard < 0 && bComponents.hard >= 0) return -1;
970
+ if (aComponents.hard >= 0 && bComponents.hard < 0) return 1;
971
+
972
+ // Then by absolute hard score
973
+ if (Math.abs(aComponents.hard) !== Math.abs(bComponents.hard)) {
974
+ return Math.abs(bComponents.hard) - Math.abs(aComponents.hard);
975
+ }
976
+
977
+ // Then by soft score
978
+ return Math.abs(bComponents.soft) - Math.abs(aComponents.soft);
979
+ });
980
+
981
+ // Build the analysis table
982
+ let html = '<table class="table table-sm">';
983
+ html += `
984
+ <thead>
985
+ <tr>
986
+ <th></th>
987
+ <th>Constraint</th>
988
+ <th>Type</th>
989
+ <th>Matches</th>
990
+ <th>Score</th>
991
+ </tr>
992
+ </thead>
993
+ <tbody>
994
+ `;
995
+
996
+ constraints.forEach(constraint => {
997
+ const components = getScoreComponents(constraint.score || "0hard/0soft");
998
+ const isHard = components.hard !== 0;
999
+ const isViolated = components.hard < 0 || components.soft < 0;
1000
+ const matchCount = constraint.matches ? constraint.matches.length : 0;
1001
+
1002
+ // Status icon
1003
+ let icon = '';
1004
+ if (isHard && components.hard < 0) {
1005
+ icon = '<i class="fas fa-exclamation-triangle text-danger"></i>';
1006
+ } else if (matchCount === 0) {
1007
+ icon = '<i class="fas fa-check-circle text-success"></i>';
1008
+ } else {
1009
+ icon = '<i class="fas fa-minus-circle text-warning"></i>';
1010
+ }
1011
+
1012
+ // Type badge
1013
+ const typeBadge = isHard
1014
+ ? '<span class="badge bg-danger">hard</span>'
1015
+ : '<span class="badge bg-success">soft</span>';
1016
+
1017
+ // Score display
1018
+ const scoreDisplay = isHard ? components.hard : components.soft;
1019
+
1020
+ html += `
1021
+ <tr>
1022
+ <td>${icon}</td>
1023
+ <td>${constraint.name}</td>
1024
+ <td>${typeBadge}</td>
1025
+ <td><strong>${matchCount}</strong></td>
1026
+ <td>${scoreDisplay}</td>
1027
+ </tr>
1028
+ `;
1029
+ });
1030
+
1031
+ html += '</tbody></table>';
1032
+ container.html(html);
1033
+ }
1034
+
1035
+ /**
1036
+ * Parses a score string into its component parts.
1037
+ *
1038
+ * EXAMPLES:
1039
+ * - "0hard/0soft" -> {hard: 0, soft: 0}
1040
+ * - "-2hard/-15soft" -> {hard: -2, soft: -15}
1041
+ *
1042
+ * @param {string} score - The score string to parse
1043
+ * @returns {Object} Object with hard, medium, soft properties
1044
+ */
1045
+ function getScoreComponents(score) {
1046
+ let components = {hard: 0, medium: 0, soft: 0};
1047
+
1048
+ if (!score || typeof score !== 'string') {
1049
+ return components;
1050
+ }
1051
+
1052
+ // Match patterns like "-2hard", "0soft", "-5medium"
1053
+ const matches = [...score.matchAll(/(-?\d*\.?\d+)(hard|medium|soft)/g)];
1054
+ matches.forEach(match => {
1055
+ components[match[2]] = parseFloat(match[1]);
1056
+ });
1057
+
1058
+ return components;
1059
+ }
1060
+
1061
+
1062
+ // =============================================================================
1063
+ // 10. TAB NAVIGATION HELPERS
1064
+ // =============================================================================
1065
+ // Functions for switching between tabs programmatically.
1066
+
1067
+ /**
1068
+ * Navigates to Build tab and shows a specific file.
1069
+ *
1070
+ * Used by code-link elements to view source code.
1071
+ *
1072
+ * @param {string} filename - The file to show in the Build tab
1073
+ */
1074
+ function showInBuild(filename) {
1075
+ // Switch to Build tab using Bootstrap 5 API
1076
+ const tabEl = document.querySelector('[data-bs-target="#build"]');
1077
+ if (tabEl) {
1078
+ const tab = new bootstrap.Tab(tabEl);
1079
+ tab.show();
1080
+ }
1081
+
1082
+ // Load the requested file after a short delay to ensure tab is visible
1083
+ setTimeout(() => {
1084
+ loadSourceFile(filename);
1085
+ }, 100);
1086
+ }
1087
+
1088
+ /**
1089
+ * Navigates from Build tab to Demo tab.
1090
+ *
1091
+ * Used by "See in Demo" button in the code viewer.
1092
+ */
1093
+ function showInDemo() {
1094
+ // Switch to Demo tab using Bootstrap 5 API
1095
+ const tabEl = document.querySelector('[data-bs-target="#demo"]');
1096
+ if (tabEl) {
1097
+ const tab = new bootstrap.Tab(tabEl);
1098
+ tab.show();
1099
+ }
1100
+ }
1101
+
1102
+
1103
+ // =============================================================================
1104
+ // 11. BUILD TAB - Source Code Viewer
1105
+ // =============================================================================
1106
+ // Functions for the source code viewer with syntax highlighting.
1107
+
1108
+ /**
1109
+ * Sets up click handlers for the file navigator.
1110
+ */
1111
+ function setupBuildTab() {
1112
+ // File item click handlers
1113
+ $('.file-item').click(function() {
1114
+ const filename = $(this).data('file');
1115
+ if (filename) {
1116
+ // Update active state
1117
+ $('.file-item').removeClass('active');
1118
+ $(this).addClass('active');
1119
+
1120
+ // Load the file
1121
+ loadSourceFile(filename);
1122
+ }
1123
+ });
1124
+ }
1125
+
1126
+ /**
1127
+ * Loads and displays a source file in the code viewer.
1128
+ *
1129
+ * FLOW:
1130
+ * 1. Fetch source code from /source-code/{filename} API
1131
+ * 2. Update the code viewer header with file path
1132
+ * 3. Set the code content and language class
1133
+ * 4. Trigger Prism.js highlighting
1134
+ * 5. If section is provided, find its line number and scroll to it
1135
+ *
1136
+ * RUNTIME LINE DETECTION:
1137
+ * When a section name is provided (e.g., "updateKPIs"), we search the loaded
1138
+ * content for patterns that indicate where that section is defined:
1139
+ * - Python: "def section_name" or "class SectionName"
1140
+ * - JavaScript: "function sectionName" or "sectionName(" or "const sectionName"
1141
+ *
1142
+ * This approach is more robust than hardcoded line numbers because:
1143
+ * - Line numbers change as code is edited
1144
+ * - Different environments might have different line endings
1145
+ * - The search adapts to the actual file content at runtime
1146
+ *
1147
+ * @param {string} filename - The file to load
1148
+ * @param {string} [section] - Optional section/function name to scroll to
1149
+ */
1150
+ function loadSourceFile(filename, section = null) {
1151
+ console.log('Loading source file:', filename, section ? `(section: ${section})` : '');
1152
+ currentFile = filename;
1153
+
1154
+ // Determine language for syntax highlighting based on file extension
1155
+ // Prism.js uses different language identifiers for different file types
1156
+ let language = 'python';
1157
+ if (filename.endsWith('.js')) {
1158
+ language = 'javascript';
1159
+ } else if (filename.endsWith('.html')) {
1160
+ language = 'markup'; // Prism uses 'markup' for HTML
1161
+ }
1162
+
1163
+ // Update header with file path and appropriate icon
1164
+ const icon = language === 'python' ? 'fab fa-python'
1165
+ : language === 'javascript' ? 'fab fa-js'
1166
+ : 'fab fa-html5';
1167
+ const path = filename.endsWith('.py')
1168
+ ? `src/my_quickstart/${filename}`
1169
+ : `static/${filename}`;
1170
+
1171
+ $('#currentFilePath').html(`<i class="${icon} me-2"></i>${path}`);
1172
+
1173
+ // Show loading state while fetching
1174
+ const codeEl = $('#codeContent');
1175
+ codeEl.text('Loading...');
1176
+
1177
+ // Fetch source code from API
1178
+ // The /source-code/{filename} endpoint returns {filename, content}
1179
+ $.getJSON(`/source-code/${filename}`, function(data) {
1180
+ currentFileContent = data.content || '// File not found';
1181
+
1182
+ // Update code content in the <code> element
1183
+ codeEl.text(currentFileContent);
1184
+ codeEl.attr('class', `language-${language}`);
1185
+
1186
+ // Trigger Prism.js syntax highlighting
1187
+ // This transforms plain text into highlighted HTML with line numbers
1188
+ if (typeof Prism !== 'undefined') {
1189
+ Prism.highlightElement(codeEl[0]);
1190
+ }
1191
+
1192
+ // RUNTIME LINE DETECTION: If a section was requested, find and scroll to it
1193
+ // We do this AFTER Prism highlighting because:
1194
+ // 1. The content needs to be rendered before we can scroll
1195
+ // 2. Prism adds line-numbers-rows elements we use for accurate scrolling
1196
+ if (section) {
1197
+ // Small delay to ensure Prism.js has finished rendering line numbers
1198
+ // Prism's highlightElement is synchronous, but DOM updates need a tick
1199
+ setTimeout(() => {
1200
+ const lineNumber = findSectionLineNumber(currentFileContent, section, language);
1201
+ if (lineNumber > 0) {
1202
+ console.log(`Found "${section}" at line ${lineNumber}`);
1203
+ scrollToLine(lineNumber);
1204
+ } else {
1205
+ console.warn(`Section "${section}" not found in ${filename}`);
1206
+ }
1207
+ }, 50);
1208
+ }
1209
+
1210
+ }).fail(function(xhr, status, error) {
1211
+ console.error('Failed to load source file:', error);
1212
+ codeEl.text('// Error loading file: ' + error);
1213
+ currentFileContent = '';
1214
+ });
1215
+ }
1216
+
1217
+
1218
+ /**
1219
+ * Finds the line number where a section (function/class) is defined.
1220
+ *
1221
+ * RUNTIME LINE DETECTION EXPLAINED:
1222
+ * ---------------------------------
1223
+ * This is the core of our "smart scroll" feature. Instead of hardcoding line
1224
+ * numbers (which break when code changes), we search the actual file content
1225
+ * at runtime to find where a function or class is defined.
1226
+ *
1227
+ * HOW IT WORKS:
1228
+ * 1. Split the file content into lines
1229
+ * 2. Build regex patterns based on the language and section name
1230
+ * 3. Search each line for a match
1231
+ * 4. Return the 1-based line number (or 0 if not found)
1232
+ *
1233
+ * PATTERNS SEARCHED:
1234
+ * - Python: "def section_name(" or "class SectionName"
1235
+ * - JavaScript: "function sectionName(" or "sectionName = function"
1236
+ * or "const/let/var sectionName" or "sectionName(" at definition
1237
+ *
1238
+ * WHY THIS APPROACH:
1239
+ * - Resilient: Works even as code is edited and line numbers change
1240
+ * - Flexible: Can find functions, classes, or any named definition
1241
+ * - Language-aware: Uses appropriate patterns for Python vs JavaScript
1242
+ *
1243
+ * TRADE-OFFS:
1244
+ * - May not find minified code or unusual formatting
1245
+ * - Could match wrong occurrence if same name appears multiple times
1246
+ * (we return the FIRST match, which is usually the definition)
1247
+ *
1248
+ * @param {string} content - The full file content
1249
+ * @param {string} sectionName - The function/class name to find
1250
+ * @param {string} language - The file language ('python', 'javascript', 'markup')
1251
+ * @returns {number} 1-based line number, or 0 if not found
1252
+ */
1253
+ function findSectionLineNumber(content, sectionName, language) {
1254
+ if (!content || !sectionName) {
1255
+ return 0;
1256
+ }
1257
+
1258
+ // Split content into lines for line-by-line search
1259
+ const lines = content.split('\n');
1260
+
1261
+ // Build search patterns based on language
1262
+ // We use multiple patterns to catch different definition styles
1263
+ const patterns = [];
1264
+
1265
+ if (language === 'python') {
1266
+ // Python patterns:
1267
+ // - "def function_name(" - function definition
1268
+ // - "class ClassName" - class definition (may or may not have parens)
1269
+ // - "@decorator" followed by def - decorated functions
1270
+ patterns.push(new RegExp(`^\\s*def\\s+${sectionName}\\s*\\(`));
1271
+ patterns.push(new RegExp(`^\\s*class\\s+${sectionName}\\b`));
1272
+ patterns.push(new RegExp(`^\\s*async\\s+def\\s+${sectionName}\\s*\\(`));
1273
+ } else if (language === 'javascript') {
1274
+ // JavaScript patterns:
1275
+ // - "function functionName(" - classic function declaration
1276
+ // - "functionName = function" - function expression
1277
+ // - "const/let/var functionName" - modern declaration
1278
+ // - "functionName(" in object/class context
1279
+ // - "async function" variants
1280
+ patterns.push(new RegExp(`^\\s*function\\s+${sectionName}\\s*\\(`));
1281
+ patterns.push(new RegExp(`^\\s*async\\s+function\\s+${sectionName}\\s*\\(`));
1282
+ patterns.push(new RegExp(`^\\s*(const|let|var)\\s+${sectionName}\\s*=`));
1283
+ patterns.push(new RegExp(`^\\s*${sectionName}\\s*[:=]\\s*(async\\s+)?function`));
1284
+ patterns.push(new RegExp(`^\\s*${sectionName}\\s*\\(`)); // Method shorthand
1285
+ } else {
1286
+ // HTML/markup: search for id or class attributes
1287
+ patterns.push(new RegExp(`id=["']${sectionName}["']`));
1288
+ patterns.push(new RegExp(`class=["'][^"']*${sectionName}[^"']*["']`));
1289
+ }
1290
+
1291
+ // Search each line for any of our patterns
1292
+ for (let i = 0; i < lines.length; i++) {
1293
+ const line = lines[i];
1294
+ for (const pattern of patterns) {
1295
+ if (pattern.test(line)) {
1296
+ // Return 1-based line number (lines array is 0-indexed)
1297
+ return i + 1;
1298
+ }
1299
+ }
1300
+ }
1301
+
1302
+ // Fallback: simple substring search for the section name
1303
+ // This catches cases our patterns missed (e.g., comments mentioning the section)
1304
+ for (let i = 0; i < lines.length; i++) {
1305
+ if (lines[i].includes(sectionName)) {
1306
+ console.log(`Fallback match for "${sectionName}" at line ${i + 1}`);
1307
+ return i + 1;
1308
+ }
1309
+ }
1310
+
1311
+ return 0; // Not found
1312
+ }
1313
+
1314
+
1315
+ /**
1316
+ * Scrolls the code viewer to a specific line.
1317
+ *
1318
+ * Uses multiple approaches to accurately scroll to a line:
1319
+ * 1. Try to find Prism.js line-numbers-rows spans
1320
+ * 2. Fall back to measuring line height from rendered code
1321
+ *
1322
+ * @param {number} lineNumber - The line to scroll to
1323
+ */
1324
+ function scrollToLine(lineNumber) {
1325
+ const viewer = $('.code-viewer-body');
1326
+ const preEl = viewer.find('pre')[0];
1327
+ const codeEl = viewer.find('code')[0];
1328
+
1329
+ if (!preEl || !codeEl) {
1330
+ console.warn('No code element found for scrolling');
1331
+ return;
1332
+ }
1333
+
1334
+ let offset = 0;
1335
+
1336
+ // Method 1: Try to use Prism.js line-numbers-rows spans
1337
+ const lineNumbersRows = preEl.querySelector('.line-numbers-rows');
1338
+ if (lineNumbersRows && lineNumbersRows.children.length > 0) {
1339
+ // Get the height of a single line number span
1340
+ const firstLineSpan = lineNumbersRows.children[0];
1341
+ if (firstLineSpan) {
1342
+ const lineHeight = firstLineSpan.getBoundingClientRect().height;
1343
+ offset = (lineNumber - 1) * lineHeight;
1344
+ console.log(`Using line-numbers-rows method: lineHeight=${lineHeight}px, offset=${offset}px`);
1345
+ }
1346
+ }
1347
+
1348
+ // Method 2: Fall back to measuring from pre element
1349
+ if (offset === 0) {
1350
+ const preStyle = window.getComputedStyle(preEl);
1351
+ let lineHeight = parseFloat(preStyle.lineHeight);
1352
+
1353
+ // If lineHeight is 'normal', compute from font size
1354
+ if (isNaN(lineHeight) || lineHeight <= 0) {
1355
+ const fontSize = parseFloat(preStyle.fontSize) || 14;
1356
+ lineHeight = fontSize * 1.5;
1357
+ }
1358
+
1359
+ offset = (lineNumber - 1) * lineHeight;
1360
+ console.log(`Using fallback method: lineHeight=${lineHeight}px, offset=${offset}px`);
1361
+ }
1362
+
1363
+ // Add padding offset from the pre element
1364
+ const preStyle = window.getComputedStyle(preEl);
1365
+ const paddingTop = parseFloat(preStyle.paddingTop) || 0;
1366
+ offset += paddingTop;
1367
+
1368
+ // Animate scroll with some padding above the target line
1369
+ const viewerHeight = viewer.height();
1370
+ const scrollTarget = Math.max(0, offset - (viewerHeight * 0.2));
1371
+
1372
+ viewer.animate({
1373
+ scrollTop: scrollTarget
1374
+ }, 300);
1375
+
1376
+ console.log(`Scrolling to line ${lineNumber}, final offset ${scrollTarget}px`);
1377
+
1378
+ // Highlight the line briefly
1379
+ highlightLine(lineNumber);
1380
+ }
1381
+
1382
+ /**
1383
+ * Briefly highlights a line in the code viewer.
1384
+ *
1385
+ * @param {number} lineNumber - The line to highlight
1386
+ */
1387
+ function highlightLine(lineNumber) {
1388
+ // Remove any existing highlights
1389
+ $('.line-highlight').remove();
1390
+
1391
+ const viewer = $('.code-viewer-body');
1392
+ const preEl = viewer.find('pre')[0];
1393
+
1394
+ if (!preEl) return;
1395
+
1396
+ // Get line height
1397
+ const lineNumbersRows = preEl.querySelector('.line-numbers-rows');
1398
+ let lineHeight = 21; // default
1399
+
1400
+ if (lineNumbersRows && lineNumbersRows.children.length > 0) {
1401
+ lineHeight = lineNumbersRows.children[0].getBoundingClientRect().height;
1402
+ }
1403
+
1404
+ const preStyle = window.getComputedStyle(preEl);
1405
+ const paddingTop = parseFloat(preStyle.paddingTop) || 0;
1406
+
1407
+ // Create highlight element
1408
+ const highlight = $('<div class="line-highlight"></div>');
1409
+ highlight.css({
1410
+ position: 'absolute',
1411
+ left: 0,
1412
+ right: 0,
1413
+ top: paddingTop + (lineNumber - 1) * lineHeight,
1414
+ height: lineHeight,
1415
+ background: 'rgba(62, 0, 255, 0.15)',
1416
+ borderLeft: '3px solid #3E00FF',
1417
+ pointerEvents: 'none',
1418
+ zIndex: 10
1419
+ });
1420
+
1421
+ // Add to pre element (needs relative positioning)
1422
+ $(preEl).css('position', 'relative').append(highlight);
1423
+
1424
+ // Fade out after 2 seconds
1425
+ setTimeout(() => {
1426
+ highlight.fadeOut(500, function() {
1427
+ $(this).remove();
1428
+ });
1429
+ }, 2000);
1430
+ }
1431
+
1432
+ /**
1433
+ * Copies the current code to clipboard.
1434
+ */
1435
+ function copyCurrentCode() {
1436
+ const code = currentFileContent || '';
1437
+
1438
+ if (!code) {
1439
+ showNotification('No code loaded to copy', 'warning');
1440
+ return;
1441
+ }
1442
+
1443
+ navigator.clipboard.writeText(code).then(() => {
1444
+ showNotification('Code copied to clipboard!', 'success');
1445
+ }).catch(err => {
1446
+ console.error('Failed to copy:', err);
1447
+ showNotification('Failed to copy code', 'danger');
1448
+ });
1449
+ }
1450
+
1451
+
1452
+ // =============================================================================
1453
+ // 12. INTERACTIVE CODE FEATURES - Click-to-Code Navigation
1454
+ // =============================================================================
1455
+ // The "code-link" feature: clicking UI elements reveals their source code.
1456
+
1457
+ /**
1458
+ * Sets up click handlers for code-link elements.
1459
+ *
1460
+ * Elements with class="code-link" and data-target="file:section"
1461
+ * will navigate to the Build tab and highlight the relevant code.
1462
+ *
1463
+ * EXAMPLES:
1464
+ * - data-target="app.js:updateKPIs" -> app.js, scrolls to updateKPIs function
1465
+ * - data-target="constraints.py:required_skill" -> constraints.py
1466
+ */
1467
+ function setupCodeLinkHandlers() {
1468
+ // Main code-link elements
1469
+ $(document).on('click', '.code-link', function(e) {
1470
+ // Don't trigger for nested clickable elements
1471
+ if ($(e.target).closest('.btn').length > 0) {
1472
+ return; // Let button clicks work normally
1473
+ }
1474
+
1475
+ const target = $(this).data('target');
1476
+ if (target) {
1477
+ navigateToCode(target);
1478
+ }
1479
+ });
1480
+
1481
+ // Constraint badges
1482
+ $(document).on('click', '.constraint-badge', function(e) {
1483
+ e.stopPropagation();
1484
+ const target = $(this).data('target');
1485
+ if (target) {
1486
+ navigateToCode(target);
1487
+ }
1488
+ });
1489
+ }
1490
+
1491
+ /**
1492
+ * Navigates to a specific code location from an code-link target.
1493
+ *
1494
+ * TARGET FORMAT: "filename:section"
1495
+ * - filename: The file to show (e.g., "app.js", "constraints.py")
1496
+ * - section: Optional section/function name to scroll to (e.g., "updateKPIs")
1497
+ *
1498
+ * RUNTIME LINE DETECTION:
1499
+ * Unlike static line numbers that would break when code changes, this function
1500
+ * uses runtime search to find the section. After the file loads from the API,
1501
+ * we search the actual content for the section name (function/class definition)
1502
+ * and scroll to where it's found. This keeps code-links working even as code evolves.
1503
+ *
1504
+ * @param {string} target - The target in "file:section" format
1505
+ */
1506
+ function navigateToCode(target) {
1507
+ console.log('Navigating to code:', target);
1508
+
1509
+ // Parse target: "filename:section" -> ["filename", "section"]
1510
+ // The section is optional - if not provided, we just show the file from the top
1511
+ const [filename, section] = target.split(':');
1512
+
1513
+ // Switch to Build tab using Bootstrap 5 Tab API
1514
+ // We access the nav-link element and create a Tab instance to show it
1515
+ const tabEl = document.querySelector('[data-bs-target="#build"]');
1516
+ if (tabEl) {
1517
+ const tab = new bootstrap.Tab(tabEl);
1518
+ tab.show();
1519
+ }
1520
+
1521
+ // Load the file after a short delay to ensure tab transition completes
1522
+ // The delay is needed because Bootstrap's tab.show() is asynchronous
1523
+ setTimeout(() => {
1524
+ // Update file navigator active state for visual feedback
1525
+ $('.file-item').removeClass('active');
1526
+ $(`.file-item[data-file="${filename}"]`).addClass('active');
1527
+
1528
+ // Load the file, passing the section name for line scrolling
1529
+ // If section is provided, loadSourceFile will search for it after loading
1530
+ loadSourceFile(filename, section);
1531
+ }, 100);
1532
+
1533
+ // Show notification with file and section info
1534
+ const sectionInfo = section ? ` → ${section}` : '';
1535
+ showNotification(`Viewing ${filename}${sectionInfo}`, 'info');
1536
+ }
1537
+
1538
+
1539
+ // =============================================================================
1540
+ // 13. NOTIFICATIONS
1541
+ // =============================================================================
1542
+ // Toast-style notification messages.
1543
+
1544
+ /**
1545
+ * Shows a notification toast message.
1546
+ *
1547
+ * TYPES:
1548
+ * - 'success': Green checkmark
1549
+ * - 'danger': Red X
1550
+ * - 'warning': Yellow warning
1551
+ * - 'info': Blue info
1552
+ *
1553
+ * @param {string} message - The message to display
1554
+ * @param {string} type - The notification type (success, danger, warning, info)
1555
+ */
1556
+ function showNotification(message, type = 'info') {
1557
+ const panel = $('#notificationPanel');
1558
+
1559
+ // Create the toast
1560
+ const toast = $(`
1561
+ <div class="alert alert-${type} alert-dismissible fade show" role="alert">
1562
+ ${message}
1563
+ <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
1564
+ </div>
1565
+ `);
1566
+
1567
+ panel.append(toast);
1568
+
1569
+ // Auto-dismiss after 5 seconds
1570
+ setTimeout(() => {
1571
+ toast.alert('close');
1572
+ }, 5000);
1573
+ }
1574
+
1575
+
1576
+ // =============================================================================
1577
+ // 14. UTILITY FUNCTIONS
1578
+ // =============================================================================
1579
+ // Helper functions used throughout the application.
1580
+
1581
+ /**
1582
+ * Escapes HTML special characters to prevent XSS.
1583
+ *
1584
+ * @param {string} text - The text to escape
1585
+ * @returns {string} Escaped text safe for HTML insertion
1586
+ */
1587
+ function escapeHtml(text) {
1588
+ const div = document.createElement('div');
1589
+ div.textContent = text;
1590
+ return div.innerHTML;
1591
+ }
1592
+ // =============================================================================
1593
+ // 15. RESOURCE & TASK CRUD OPERATIONS
1594
+ // =============================================================================
1595
+ // Functions for adding and removing resources and tasks dynamically.
1596
+
1597
+ /**
1598
+ * Shows the Add Resource modal dialog.
1599
+ *
1600
+ * @param {Event} event - Click event (to stop propagation)
1601
+ */
1602
+ function showAddResourceModal(event) {
1603
+ event.stopPropagation();
1604
+
1605
+ // Clear previous values
1606
+ $('#resourceName').val('');
1607
+ $('#resourceCapacity').val(100);
1608
+ $('#resourceSkills').val('');
1609
+
1610
+ // Show modal
1611
+ const modal = new bootstrap.Modal('#addResourceModal');
1612
+ modal.show();
1613
+ }
1614
+
1615
+ /**
1616
+ * Adds a new resource to the schedule.
1617
+ *
1618
+ * Reads values from the Add Resource modal form and adds
1619
+ * a new resource to loadedSchedule.resources.
1620
+ */
1621
+ function addResource() {
1622
+ const name = $('#resourceName').val().trim();
1623
+ const capacity = parseInt($('#resourceCapacity').val()) || 100;
1624
+ const skillsStr = $('#resourceSkills').val().trim();
1625
+ const skills = skillsStr ? skillsStr.split(',').map(s => s.trim().toLowerCase()) : [];
1626
+
1627
+ // Validate
1628
+ if (!name) {
1629
+ showNotification('Please enter a resource name', 'warning');
1630
+ return;
1631
+ }
1632
+
1633
+ // Check for duplicate
1634
+ if (loadedSchedule && loadedSchedule.resources) {
1635
+ if (loadedSchedule.resources.some(r => r.name === name)) {
1636
+ showNotification('A resource with this name already exists', 'warning');
1637
+ return;
1638
+ }
1639
+ }
1640
+
1641
+ // Initialize schedule if needed
1642
+ if (!loadedSchedule) {
1643
+ loadedSchedule = { resources: [], tasks: [] };
1644
+ }
1645
+ if (!loadedSchedule.resources) {
1646
+ loadedSchedule.resources = [];
1647
+ }
1648
+
1649
+ // Add the resource
1650
+ loadedSchedule.resources.push({
1651
+ name: name,
1652
+ capacity: capacity,
1653
+ skills: skills
1654
+ });
1655
+
1656
+ // Close modal and re-render
1657
+ bootstrap.Modal.getInstance('#addResourceModal').hide();
1658
+ renderSchedule(loadedSchedule);
1659
+ showNotification(`Added resource: ${name}`, 'success');
1660
+ }
1661
+
1662
+ /**
1663
+ * Removes a resource from the schedule.
1664
+ *
1665
+ * @param {string} resourceName - Name of the resource to remove
1666
+ * @param {Event} event - Click event (to stop propagation)
1667
+ */
1668
+ function removeResource(resourceName, event) {
1669
+ event.stopPropagation();
1670
+
1671
+ if (!loadedSchedule || !loadedSchedule.resources) {
1672
+ return;
1673
+ }
1674
+
1675
+ // Remove the resource
1676
+ loadedSchedule.resources = loadedSchedule.resources.filter(r => r.name !== resourceName);
1677
+
1678
+ // Unassign any tasks assigned to this resource
1679
+ if (loadedSchedule.tasks) {
1680
+ loadedSchedule.tasks.forEach(task => {
1681
+ if (task.resource === resourceName) {
1682
+ task.resource = null;
1683
+ }
1684
+ });
1685
+ }
1686
+
1687
+ // Re-render
1688
+ renderSchedule(loadedSchedule);
1689
+ showNotification(`Removed resource: ${resourceName}`, 'info');
1690
+ }
1691
+
1692
+ /**
1693
+ * Shows the Add Task modal dialog.
1694
+ *
1695
+ * @param {Event} event - Click event (to stop propagation)
1696
+ */
1697
+ function showAddTaskModal(event) {
1698
+ event.stopPropagation();
1699
+
1700
+ // Clear previous values
1701
+ $('#taskName').val('');
1702
+ $('#taskDuration').val(30);
1703
+ $('#taskSkill').val('');
1704
+
1705
+ // Show modal
1706
+ const modal = new bootstrap.Modal('#addTaskModal');
1707
+ modal.show();
1708
+ }
1709
+
1710
+ /**
1711
+ * Adds a new task to the schedule.
1712
+ *
1713
+ * Reads values from the Add Task modal form and adds
1714
+ * a new task to loadedSchedule.tasks.
1715
+ */
1716
+ function addTask() {
1717
+ const name = $('#taskName').val().trim();
1718
+ const duration = parseInt($('#taskDuration').val()) || 30;
1719
+ const requiredSkill = $('#taskSkill').val().trim().toLowerCase();
1720
+
1721
+ // Validate
1722
+ if (!name) {
1723
+ showNotification('Please enter a task name', 'warning');
1724
+ return;
1725
+ }
1726
+
1727
+ // Initialize schedule if needed
1728
+ if (!loadedSchedule) {
1729
+ loadedSchedule = { resources: [], tasks: [] };
1730
+ }
1731
+ if (!loadedSchedule.tasks) {
1732
+ loadedSchedule.tasks = [];
1733
+ }
1734
+
1735
+ // Generate unique ID
1736
+ const existingIds = loadedSchedule.tasks.map(t => t.id);
1737
+ let newId = `task-${loadedSchedule.tasks.length + 1}`;
1738
+ let counter = loadedSchedule.tasks.length + 1;
1739
+ while (existingIds.includes(newId)) {
1740
+ counter++;
1741
+ newId = `task-${counter}`;
1742
+ }
1743
+
1744
+ // Add the task
1745
+ loadedSchedule.tasks.push({
1746
+ id: newId,
1747
+ name: name,
1748
+ duration: duration,
1749
+ requiredSkill: requiredSkill || '',
1750
+ resource: null
1751
+ });
1752
+
1753
+ // Close modal and re-render
1754
+ bootstrap.Modal.getInstance('#addTaskModal').hide();
1755
+ renderSchedule(loadedSchedule);
1756
+ showNotification(`Added task: ${name}`, 'success');
1757
+ }
1758
+
1759
+ /**
1760
+ * Removes a task from the schedule.
1761
+ *
1762
+ * @param {string} taskId - ID of the task to remove
1763
+ * @param {Event} event - Click event (to stop propagation)
1764
+ */
1765
+ function removeTask(taskId, event) {
1766
+ event.stopPropagation();
1767
+
1768
+ if (!loadedSchedule || !loadedSchedule.tasks) {
1769
+ return;
1770
+ }
1771
+
1772
+ // Find task name for notification
1773
+ const task = loadedSchedule.tasks.find(t => t.id === taskId);
1774
+ const taskName = task ? task.name : taskId;
1775
+
1776
+ // Remove the task
1777
+ loadedSchedule.tasks = loadedSchedule.tasks.filter(t => t.id !== taskId);
1778
+
1779
+ // Re-render
1780
+ renderSchedule(loadedSchedule);
1781
+ showNotification(`Removed task: ${taskName}`, 'info');
1782
+ }
1783
+
1784
+
1785
+ // =============================================================================
1786
+ // 16. CONSTRAINT WEIGHT CONTROLS
1787
+ // =============================================================================
1788
+ // Functions for adjusting constraint weights via sliders.
1789
+
1790
+ /**
1791
+ * Default constraint weight values.
1792
+ * Hard constraints default to 100, soft constraints to 50.
1793
+ */
1794
+ const DEFAULT_WEIGHTS = {
1795
+ RequiredSkill: 100,
1796
+ ResourceCapacity: 100,
1797
+ MinimizeDuration: 50,
1798
+ BalanceLoad: 50
1799
+ };
1800
+
1801
+ /**
1802
+ * Updates the displayed value for a constraint weight slider.
1803
+ *
1804
+ * Called by oninput on each slider.
1805
+ *
1806
+ * @param {string} constraintName - Name of the constraint (e.g., "RequiredSkill")
1807
+ */
1808
+ function updateWeightDisplay(constraintName) {
1809
+ const value = $(`#weight${constraintName}`).val();
1810
+ $(`#weight${constraintName}Value`).text(value);
1811
+ }
1812
+
1813
+ /**
1814
+ * Resets all constraint weights to their default values.
1815
+ */
1816
+ function resetConstraintWeights() {
1817
+ Object.keys(DEFAULT_WEIGHTS).forEach(name => {
1818
+ $(`#weight${name}`).val(DEFAULT_WEIGHTS[name]);
1819
+ $(`#weight${name}Value`).text(DEFAULT_WEIGHTS[name]);
1820
+ });
1821
+ showNotification('Constraint weights reset to defaults', 'info');
1822
+ }
1823
+
1824
+ /**
1825
+ * Gets the current constraint weights from the sliders.
1826
+ *
1827
+ * @returns {Object} Object with constraint names and their weights (0-100)
1828
+ */
1829
+ function getConstraintWeights() {
1830
+ return {
1831
+ required_skill: parseInt($('#weightRequiredSkill').val()) || 100,
1832
+ resource_capacity: parseInt($('#weightResourceCapacity').val()) || 100,
1833
+ minimize_duration: parseInt($('#weightMinimizeDuration').val()) || 50,
1834
+ balance_load: parseInt($('#weightBalanceLoad').val()) || 50
1835
+ };
1836
+ }
static/index.html ADDED
@@ -0,0 +1,856 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <!--
4
+ ================================================================================
5
+ SOLVERFORGE QUICKSTART TEMPLATE - index.html
6
+ ================================================================================
7
+
8
+ This is an interactive educational UI template. It serves two purposes:
9
+ 1. A working UI for your SolverForge optimization problem
10
+ 2. A teaching tool that shows how to build itself
11
+
12
+ HOW TO CUSTOMIZE THIS TEMPLATE:
13
+ 1. Replace "Task Assignment Solver" with your problem name
14
+ 2. Update the KPI cards to match your domain metrics
15
+ 3. Modify the task/resource rendering to match your entities
16
+ 4. Update constraint badges to match your constraints.py
17
+
18
+ STRUCTURE:
19
+ - <head>: CSS dependencies and custom styles
20
+ - <body>:
21
+ - Header with logo and navigation
22
+ - Demo tab: Live solver visualization
23
+ - Build tab: Source code viewer
24
+ - Guide tab: cURL examples
25
+ - API tab: Swagger UI
26
+
27
+ INTERACTIVE CODE LINKS:
28
+ Elements with class="code-link" and data-target="file:section"
29
+ will show a code icon on hover. Clicking navigates to the Build tab and
30
+ highlights the relevant source code - helping you learn by exploring.
31
+ ================================================================================
32
+ -->
33
+ <head>
34
+ <meta charset="UTF-8">
35
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
36
+
37
+ <!-- =======================================================================
38
+ PAGE TITLE
39
+ TODO: Change this to match your quickstart name
40
+ ======================================================================= -->
41
+ <title>SolverForge Quickstart Template - Learn by Building</title>
42
+
43
+ <!-- =======================================================================
44
+ CSS DEPENDENCIES
45
+
46
+ Required libraries:
47
+ - Bootstrap 5.3: Layout, components, utilities
48
+ - Font Awesome 6.5: Icons
49
+ - Prism.js Tomorrow theme: Syntax highlighting for code viewer
50
+ - SolverForge webui.css: Brand colors and standard styles
51
+
52
+ TIP: These are loaded from CDN for simplicity. In production, you may
53
+ want to bundle them locally for offline use.
54
+ ======================================================================= -->
55
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.3/css/bootstrap.min.css"/>
56
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css"/>
57
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism-tomorrow.min.css"/>
58
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/plugins/line-numbers/prism-line-numbers.min.css"/>
59
+ <link rel="stylesheet" href="/webjars/solverforge/css/solverforge-webui.css"/>
60
+ <link rel="stylesheet" href="/app.css"/>
61
+ <link rel="icon" href="/webjars/solverforge/img/solverforge-favicon.svg" type="image/svg+xml">
62
+ </head>
63
+
64
+ <body>
65
+
66
+ <!--
67
+ ================================================================================
68
+ HEADER
69
+ ================================================================================
70
+
71
+ The header contains:
72
+ 1. SolverForge logo (links to solverforge.org)
73
+ 2. Main navigation tabs (Demo, Learn, Build, Guide, API)
74
+ 3. Data dropdown (populated by app.js from /demo-data endpoint)
75
+
76
+ CUSTOMIZATION:
77
+ - The logo is loaded from /webjars/solverforge/img/
78
+ - Navigation uses Bootstrap 5 pills with custom .nav-main styling
79
+ - Add/remove tabs by modifying the <ul class="nav"> list
80
+ ================================================================================
81
+ -->
82
+ <header class="bg-white shadow-sm">
83
+ <div class="container-fluid py-3">
84
+ <div class="d-flex justify-content-between align-items-center flex-wrap gap-3">
85
+
86
+ <!-- Logo - Links to SolverForge homepage -->
87
+ <a href="https://www.solverforge.org" class="text-decoration-none">
88
+ <img src="/webjars/solverforge/img/solverforge-horizontal.svg" alt="SolverForge" height="40">
89
+ </a>
90
+
91
+ <!--
92
+ NAVIGATION TABS
93
+
94
+ Each tab corresponds to a .tab-pane in the main content area.
95
+ Bootstrap's data-bs-toggle="pill" handles tab switching.
96
+
97
+ TO ADD A NEW TAB:
98
+ 1. Add a new <li class="nav-item"> here
99
+ 2. Add a matching <div class="tab-pane" id="your-tab"> below
100
+ -->
101
+ <ul class="nav nav-main" role="tablist">
102
+ <li class="nav-item">
103
+ <button class="nav-link active" data-bs-toggle="pill" data-bs-target="#demo">
104
+ <i class="fas fa-play-circle"></i>Demo
105
+ </button>
106
+ </li>
107
+ <li class="nav-item">
108
+ <button class="nav-link" data-bs-toggle="pill" data-bs-target="#build">
109
+ <i class="fas fa-code"></i>Build
110
+ </button>
111
+ </li>
112
+ <li class="nav-item">
113
+ <button class="nav-link" data-bs-toggle="pill" data-bs-target="#guide">
114
+ <i class="fas fa-book"></i>Guide
115
+ </button>
116
+ </li>
117
+ <li class="nav-item">
118
+ <button class="nav-link" data-bs-toggle="pill" data-bs-target="#api">
119
+ <i class="fas fa-plug"></i>API
120
+ </button>
121
+ </li>
122
+ </ul>
123
+
124
+ <!--
125
+ DATA DROPDOWN
126
+
127
+ Populated by app.js from the /demo-data endpoint.
128
+ Each item loads a different sample dataset.
129
+
130
+ See: loadDemoData() in app.js
131
+ -->
132
+ <div class="dropdown">
133
+ <button class="btn btn-success dropdown-toggle" type="button" data-bs-toggle="dropdown">
134
+ <i class="fas fa-database me-1"></i>Data
135
+ </button>
136
+ <ul class="dropdown-menu" id="dataDropdown">
137
+ <!-- Items added by app.js - see loadDemoData() -->
138
+ </ul>
139
+ </div>
140
+ </div>
141
+ </div>
142
+ </header>
143
+
144
+ <!--
145
+ ================================================================================
146
+ NOTIFICATION PANEL
147
+ ================================================================================
148
+
149
+ Fixed-position container for toast notifications.
150
+ Use showNotification(message, type) from app.js to display messages.
151
+
152
+ Types: 'success', 'danger', 'warning', 'info'
153
+ ================================================================================
154
+ -->
155
+ <div id="notificationPanel"></div>
156
+
157
+ <!--
158
+ ================================================================================
159
+ MAIN CONTENT - TAB PANELS
160
+ ================================================================================
161
+
162
+ Each tab-pane corresponds to a navigation tab above.
163
+ Bootstrap handles showing/hiding based on which tab is active.
164
+ ================================================================================
165
+ -->
166
+ <div class="tab-content p-4">
167
+
168
+ <!--
169
+ ===========================================================================
170
+ DEMO TAB
171
+ ===========================================================================
172
+
173
+ The main interactive solver visualization.
174
+
175
+ SECTIONS:
176
+ 1. Hero: Problem title and description
177
+ 2. KPIs: Key metrics (tasks, assigned, violations, score)
178
+ 3. Control Bar: Solve/Stop buttons
179
+ 4. Resources Panel: List of available resources
180
+ 5. Tasks Panel: Grid of tasks with assignments
181
+ 6. Constraints Legend: Clickable constraint badges
182
+
183
+ INTERACTIVE CODE FEATURE:
184
+ All major elements have class="code-link" and data-target
185
+ attributes. Clicking reveals the source code that generates them.
186
+ ===========================================================================
187
+ -->
188
+ <div class="tab-pane fade show active" id="demo">
189
+
190
+ <!--
191
+ HERO SECTION
192
+
193
+ The prominent banner introducing your optimization problem.
194
+
195
+ TODO: Customize these values for your domain:
196
+ - Change the icon (fa-project-diagram) to match your problem
197
+ - Update the title (Task Assignment Solver)
198
+ - Modify the description text
199
+
200
+ INTERACTIVE CODE: data-target="index.html:hero-section" links to this HTML
201
+ -->
202
+ <div class="hero-section mb-4 code-link" data-target="index.html:hero-section">
203
+ <span class="code-tooltip">Click to view HTML code</span>
204
+ <span class="hero-badge"><i class="fas fa-flask me-1"></i>Interactive Demo</span>
205
+ <h1 class="text-white mb-2">
206
+ <i class="fas fa-project-diagram me-2"></i>Task Assignment Solver
207
+ </h1>
208
+ <p class="text-white-50 mb-0">
209
+ Assign tasks to resources while respecting skill requirements and capacity constraints.
210
+ <strong class="text-white">Click any element</strong> to see its source code.
211
+ </p>
212
+ </div>
213
+
214
+ <!--
215
+ KPI CARDS
216
+
217
+ Display key metrics about the current solution.
218
+
219
+ DEFAULT KPIs (customize for your domain):
220
+ - kpiTotalTasks: Total planning entities
221
+ - kpiAssigned: Entities with assigned planning variable
222
+ - kpiViolations: Hard constraint violations
223
+ - kpiScore: HardSoftScore string
224
+
225
+ Each card has code-link pointing to updateKPIs() in app.js.
226
+
227
+ TO ADD A NEW KPI:
228
+ 1. Add a new col-6 col-md-3 div here
229
+ 2. Update updateKPIs() in app.js to populate it
230
+ -->
231
+ <div class="row g-3 mb-4">
232
+ <!-- Total Tasks KPI -->
233
+ <div class="col-6 col-md-3">
234
+ <div class="kpi-card kpi-tasks code-link" data-target="app.js:updateKPIs">
235
+ <span class="code-tooltip">updateKPIs() in app.js</span>
236
+ <div class="kpi-icon"><i class="fas fa-tasks"></i></div>
237
+ <div class="kpi-value" id="kpiTotalTasks">0</div>
238
+ <div class="kpi-label">Total Tasks</div>
239
+ </div>
240
+ </div>
241
+
242
+ <!-- Assigned Tasks KPI -->
243
+ <div class="col-6 col-md-3">
244
+ <div class="kpi-card kpi-assigned code-link" data-target="app.js:updateKPIs">
245
+ <span class="code-tooltip">updateKPIs() in app.js</span>
246
+ <div class="kpi-icon"><i class="fas fa-check-circle"></i></div>
247
+ <div class="kpi-value" id="kpiAssigned">0</div>
248
+ <div class="kpi-label">Assigned</div>
249
+ </div>
250
+ </div>
251
+
252
+ <!-- Violations KPI -->
253
+ <div class="col-6 col-md-3">
254
+ <div class="kpi-card kpi-violations code-link" data-target="app.js:countViolations">
255
+ <span class="code-tooltip">countViolations() in app.js</span>
256
+ <div class="kpi-icon"><i class="fas fa-exclamation-triangle"></i></div>
257
+ <div class="kpi-value" id="kpiViolations">0</div>
258
+ <div class="kpi-label">Violations</div>
259
+ </div>
260
+ </div>
261
+
262
+ <!-- Score KPI -->
263
+ <div class="col-6 col-md-3">
264
+ <div class="kpi-card kpi-score code-link" data-target="app.js:renderSchedule">
265
+ <span class="code-tooltip">renderSchedule() in app.js</span>
266
+ <div class="kpi-icon"><i class="fas fa-star"></i></div>
267
+ <div class="kpi-value" id="kpiScore">?</div>
268
+ <div class="kpi-label">Score</div>
269
+ </div>
270
+ </div>
271
+ </div>
272
+
273
+ <!--
274
+ CONTROL BAR
275
+
276
+ Contains buttons for controlling the solver.
277
+
278
+ BUTTONS:
279
+ - #solveButton: Starts solving (calls solve() in app.js)
280
+ - #stopSolvingButton: Stops solving (calls stopSolving() in app.js)
281
+ - #solvingSpinner: Shows while solver is running
282
+ - #analyzeButton: Shows score breakdown (calls analyze() in app.js)
283
+
284
+ The stop button is hidden by default, shown when solving starts.
285
+ -->
286
+ <div class="control-bar mb-4 d-flex align-items-center gap-3 flex-wrap code-link" data-target="app.js:solve">
287
+ <span class="code-tooltip">solve() and stopSolving() in app.js</span>
288
+
289
+ <!-- Solve button - visible when not solving -->
290
+ <button id="solveButton" class="btn btn-success btn-lg">
291
+ <i class="fas fa-play me-2"></i>Solve
292
+ </button>
293
+
294
+ <!-- Stop button - visible when solving -->
295
+ <button id="stopSolvingButton" class="btn btn-danger btn-lg" style="display: none;">
296
+ <i class="fas fa-stop me-2"></i>Stop
297
+ </button>
298
+
299
+ <!-- Spinner - animated when solving -->
300
+ <span id="solvingSpinner"></span>
301
+
302
+ <div class="ms-auto d-flex align-items-center gap-2">
303
+ <!-- Analyze button - shows score breakdown -->
304
+ <button id="analyzeButton" class="btn btn-outline-secondary code-link" data-target="app.js:analyze">
305
+ <span class="code-tooltip">analyze() in app.js</span>
306
+ <i class="fas fa-microscope me-1"></i>Analyze
307
+ </button>
308
+ </div>
309
+ </div>
310
+
311
+ <!--
312
+ MAIN CONTENT GRID
313
+
314
+ Two-column layout:
315
+ - Left (col-lg-4): Resources panel - problem facts
316
+ - Right (col-lg-8): Tasks panel - planning entities
317
+
318
+ Both panels have code-link pointing to their render functions.
319
+ -->
320
+ <div class="row g-4">
321
+ <!--
322
+ RESOURCES PANEL
323
+
324
+ Displays problem facts (resources that tasks can be assigned to).
325
+ Content populated by renderResources() in app.js.
326
+
327
+ Each resource shows:
328
+ - Name
329
+ - Capacity with utilization bar
330
+ - Available skills
331
+ -->
332
+ <div class="col-lg-4">
333
+ <div class="section-card code-link" data-target="app.js:renderResources">
334
+ <span class="code-tooltip">renderResources() in app.js</span>
335
+ <div class="section-header">
336
+ <h5><i class="fas fa-users me-2"></i>Resources</h5>
337
+ <div class="d-flex align-items-center gap-2">
338
+ <span class="badge bg-secondary" id="resourceCount">0</span>
339
+ <button class="btn btn-sm btn-success" onclick="showAddResourceModal(event)" title="Add Resource">
340
+ <i class="fas fa-plus"></i>
341
+ </button>
342
+ </div>
343
+ </div>
344
+ <div class="section-body" id="resourcesPanel">
345
+ <p class="text-muted text-center">Select a dataset to load resources</p>
346
+ </div>
347
+ </div>
348
+ </div>
349
+
350
+ <!--
351
+ TASKS PANEL
352
+
353
+ Displays planning entities (tasks to be assigned).
354
+ Content populated by renderSolution() in app.js.
355
+
356
+ Each task shows:
357
+ - Name and ID
358
+ - Duration
359
+ - Required skill (if any)
360
+ - Assigned resource (or "Unassigned")
361
+ - Violation status (red border if constraint violated)
362
+ -->
363
+ <div class="col-lg-8">
364
+ <div class="section-card code-link" data-target="app.js:renderSolution">
365
+ <span class="code-tooltip">renderSolution() in app.js</span>
366
+ <div class="section-header">
367
+ <h5><i class="fas fa-clipboard-list me-2"></i>Tasks</h5>
368
+ <div class="d-flex align-items-center gap-2">
369
+ <span class="badge bg-secondary" id="taskCount">0</span>
370
+ <button class="btn btn-sm btn-success" onclick="showAddTaskModal(event)" title="Add Task">
371
+ <i class="fas fa-plus"></i>
372
+ </button>
373
+ </div>
374
+ </div>
375
+ <div class="section-body" id="tasksPanel">
376
+ <p class="text-muted text-center">Select a dataset to load tasks</p>
377
+ </div>
378
+ </div>
379
+ </div>
380
+ </div>
381
+
382
+ <!--
383
+ CONSTRAINTS LEGEND
384
+
385
+ Clickable badges showing all active constraints from constraints.py.
386
+ Each badge links to the constraint function definition.
387
+
388
+ CONSTRAINT TYPES:
389
+ - Hard (red, lock icon): MUST be satisfied. Violations make solution infeasible.
390
+ - Soft (green, feather icon): SHOULD be optimized. More violations = worse score.
391
+
392
+ TODO: Update these badges to match your constraints.py definitions.
393
+ Each data-target points to "constraints.py:function_name"
394
+ -->
395
+ <div class="constraints-legend mt-4 code-link" data-target="constraints.py:define_constraints">
396
+ <span class="code-tooltip">constraints.py - define_constraints()</span>
397
+ <h6 class="mb-3"><i class="fas fa-balance-scale me-2"></i>Active Constraints</h6>
398
+ <div class="d-flex flex-wrap gap-2">
399
+ <!-- Hard constraint: Required skill -->
400
+ <span class="constraint-badge hard" data-target="constraints.py:required_skill">
401
+ <i class="fas fa-lock"></i>Required Skill
402
+ </span>
403
+
404
+ <!-- Hard constraint: Resource capacity -->
405
+ <span class="constraint-badge hard" data-target="constraints.py:resource_capacity">
406
+ <i class="fas fa-lock"></i>Resource Capacity
407
+ </span>
408
+
409
+ <!-- Soft constraint: Minimize duration -->
410
+ <span class="constraint-badge soft" data-target="constraints.py:minimize_total_duration">
411
+ <i class="fas fa-feather"></i>Minimize Duration
412
+ </span>
413
+
414
+ <!-- Soft constraint: Balance load -->
415
+ <span class="constraint-badge soft" data-target="constraints.py:balance_resource_load">
416
+ <i class="fas fa-feather"></i>Balance Load
417
+ </span>
418
+ </div>
419
+ </div>
420
+
421
+ <!--
422
+ CONSTRAINT WEIGHT SLIDERS
423
+
424
+ Interactive sliders to adjust constraint weights.
425
+ Weight 0 = disabled, 100 = full strength.
426
+ Changes are applied when clicking Solve.
427
+ -->
428
+ <div class="constraints-legend mt-3">
429
+ <div class="d-flex justify-content-between align-items-center mb-3">
430
+ <h6 class="mb-0"><i class="fas fa-sliders-h me-2"></i>Constraint Weights</h6>
431
+ <button class="btn btn-sm btn-outline-secondary" onclick="resetConstraintWeights()">
432
+ <i class="fas fa-undo me-1"></i>Reset
433
+ </button>
434
+ </div>
435
+
436
+ <!-- Hard constraint: Required Skill -->
437
+ <div class="mb-3">
438
+ <div class="d-flex justify-content-between mb-1">
439
+ <label class="form-label mb-0">
440
+ <span class="badge bg-danger me-1">Hard</span>Required Skill
441
+ </label>
442
+ <span class="text-muted" id="weightRequiredSkillValue">100</span>
443
+ </div>
444
+ <input type="range" class="form-range" id="weightRequiredSkill" min="0" max="100" value="100"
445
+ oninput="updateWeightDisplay('RequiredSkill')">
446
+ </div>
447
+
448
+ <!-- Hard constraint: Resource Capacity -->
449
+ <div class="mb-3">
450
+ <div class="d-flex justify-content-between mb-1">
451
+ <label class="form-label mb-0">
452
+ <span class="badge bg-danger me-1">Hard</span>Resource Capacity
453
+ </label>
454
+ <span class="text-muted" id="weightResourceCapacityValue">100</span>
455
+ </div>
456
+ <input type="range" class="form-range" id="weightResourceCapacity" min="0" max="100" value="100"
457
+ oninput="updateWeightDisplay('ResourceCapacity')">
458
+ </div>
459
+
460
+ <!-- Soft constraint: Minimize Duration -->
461
+ <div class="mb-3">
462
+ <div class="d-flex justify-content-between mb-1">
463
+ <label class="form-label mb-0">
464
+ <span class="badge bg-success me-1">Soft</span>Minimize Duration
465
+ </label>
466
+ <span class="text-muted" id="weightMinimizeDurationValue">50</span>
467
+ </div>
468
+ <input type="range" class="form-range" id="weightMinimizeDuration" min="0" max="100" value="50"
469
+ oninput="updateWeightDisplay('MinimizeDuration')">
470
+ </div>
471
+
472
+ <!-- Soft constraint: Balance Load -->
473
+ <div class="mb-3">
474
+ <div class="d-flex justify-content-between mb-1">
475
+ <label class="form-label mb-0">
476
+ <span class="badge bg-success me-1">Soft</span>Balance Load
477
+ </label>
478
+ <span class="text-muted" id="weightBalanceLoadValue">50</span>
479
+ </div>
480
+ <input type="range" class="form-range" id="weightBalanceLoad" min="0" max="100" value="50"
481
+ oninput="updateWeightDisplay('BalanceLoad')">
482
+ </div>
483
+
484
+ <small class="text-muted">
485
+ <i class="fas fa-info-circle me-1"></i>
486
+ Set to 0 to disable a constraint. Changes apply on next Solve.
487
+ </small>
488
+ </div>
489
+ </div>
490
+
491
+ <!--
492
+ ===========================================================================
493
+ BUILD TAB
494
+ ===========================================================================
495
+
496
+ Source code viewer showing actual quickstart files with syntax highlighting.
497
+
498
+ COMPONENTS:
499
+ 1. File Navigator (left): Tree view of project files
500
+ 2. Code Viewer (right): Syntax-highlighted code display
501
+
502
+ HOW IT WORKS:
503
+ 1. User clicks a file in the navigator
504
+ 2. app.js fetches source code from /source-code/{filename} API
505
+ 3. Prism.js highlights the syntax
506
+
507
+ INTERACTIVE CODE INTEGRATION:
508
+ When user clicks a code-link element in Demo tab,
509
+ they're navigated here with the relevant file displayed.
510
+ ===========================================================================
511
+ -->
512
+ <div class="tab-pane fade" id="build">
513
+ <div class="row g-4">
514
+ <!--
515
+ FILE NAVIGATOR
516
+
517
+ Tree view showing all project source files.
518
+ Click a file to load it in the code viewer.
519
+
520
+ Each .file-item has:
521
+ - data-file: The filename to load
522
+ - Icon: Language-specific (fa-python, fa-html5, fa-js)
523
+ - Badge: File type description
524
+
525
+ TO ADD A NEW FILE:
526
+ 1. Add a .file-item div with data-file="filename"
527
+ 2. Add the file to SOURCE_FILES whitelist in rest_api.py
528
+ -->
529
+ <div class="col-lg-3">
530
+ <div class="file-nav">
531
+ <div class="file-nav-header">
532
+ <i class="fas fa-folder-tree me-2"></i>Project Files
533
+ </div>
534
+ <div class="file-tree">
535
+ <!-- Python source files -->
536
+ <div class="folder-item">
537
+ <div class="folder-name"><i class="fas fa-folder me-2"></i>src/my_quickstart/</div>
538
+ <div class="file-item active" data-file="domain.py">
539
+ <i class="fab fa-python"></i>
540
+ <span>domain.py</span>
541
+ <span class="file-badge">Model</span>
542
+ </div>
543
+ <div class="file-item" data-file="constraints.py">
544
+ <i class="fab fa-python"></i>
545
+ <span>constraints.py</span>
546
+ <span class="file-badge">Rules</span>
547
+ </div>
548
+ <div class="file-item" data-file="solver.py">
549
+ <i class="fab fa-python"></i>
550
+ <span>solver.py</span>
551
+ <span class="file-badge">Config</span>
552
+ </div>
553
+ <div class="file-item" data-file="rest_api.py">
554
+ <i class="fab fa-python"></i>
555
+ <span>rest_api.py</span>
556
+ <span class="file-badge">API</span>
557
+ </div>
558
+ <div class="file-item" data-file="demo_data.py">
559
+ <i class="fab fa-python"></i>
560
+ <span>demo_data.py</span>
561
+ <span class="file-badge">Data</span>
562
+ </div>
563
+ </div>
564
+ <!-- Frontend files -->
565
+ <div class="folder-item mt-2">
566
+ <div class="folder-name"><i class="fas fa-folder me-2"></i>static/</div>
567
+ <div class="file-item" data-file="index.html">
568
+ <i class="fab fa-html5"></i>
569
+ <span>index.html</span>
570
+ <span class="file-badge">UI</span>
571
+ </div>
572
+ <div class="file-item" data-file="app.js">
573
+ <i class="fab fa-js"></i>
574
+ <span>app.js</span>
575
+ <span class="file-badge">Logic</span>
576
+ </div>
577
+ <div class="file-item" data-file="app.css">
578
+ <i class="fab fa-css3-alt"></i>
579
+ <span>app.css</span>
580
+ <span class="file-badge">Styles</span>
581
+ </div>
582
+ </div>
583
+ </div>
584
+ </div>
585
+
586
+ </div>
587
+
588
+ <!--
589
+ CODE VIEWER
590
+
591
+ Main panel showing syntax-highlighted source code.
592
+
593
+ Components:
594
+ - Header: File path and action buttons
595
+ - Body: Prism.js highlighted code with line numbers
596
+
597
+ Action buttons:
598
+ - Copy: Copies code to clipboard
599
+ - See in Demo: Navigates to Demo tab (useful for HTML/JS)
600
+ -->
601
+ <div class="col-lg-9">
602
+ <div class="code-viewer">
603
+ <div class="code-viewer-header">
604
+ <span class="code-path" id="currentFilePath">
605
+ <i class="fab fa-python me-2"></i>src/my_quickstart/domain.py
606
+ </span>
607
+ <div>
608
+ <button class="btn btn-sm btn-outline-light me-2" onclick="copyCurrentCode()">
609
+ <i class="fas fa-copy me-1"></i>Copy
610
+ </button>
611
+ <button class="btn btn-sm btn-outline-success" onclick="showInDemo()">
612
+ <i class="fas fa-eye me-1"></i>See in Demo
613
+ </button>
614
+ </div>
615
+ </div>
616
+ <div class="code-viewer-body">
617
+ <!--
618
+ Prism.js handles syntax highlighting.
619
+ The .line-numbers class enables line number display.
620
+ Language class (language-python) sets highlighting rules.
621
+ -->
622
+ <pre class="line-numbers"><code class="language-python" id="codeContent">Loading...</code></pre>
623
+ </div>
624
+ </div>
625
+ </div>
626
+ </div>
627
+ </div>
628
+
629
+ <!--
630
+ ===========================================================================
631
+ GUIDE TAB
632
+ ===========================================================================
633
+
634
+ REST API integration guide with cURL examples.
635
+
636
+ Shows the typical workflow:
637
+ 1. Download demo data
638
+ 2. Start solving (get job ID)
639
+ 3. Check status
640
+ 4. Get solution
641
+ 5. Stop solving
642
+
643
+ TODO: Update endpoint paths and examples to match your API.
644
+ ===========================================================================
645
+ -->
646
+ <div class="tab-pane fade" id="guide">
647
+ <div class="container">
648
+ <h1 class="mb-4"><i class="fas fa-book me-2"></i>REST API Guide</h1>
649
+ <h2>Integration via cURL</h2>
650
+
651
+ <h3 class="mt-4">1. Download demo data</h3>
652
+ <pre class="bg-dark text-light p-3 rounded"><code>curl -X GET http://localhost:8080/demo-data/SMALL -o sample.json</code></pre>
653
+
654
+ <h3 class="mt-4">2. Start solving</h3>
655
+ <p>Returns a <code>jobId</code> for subsequent requests.</p>
656
+ <pre class="bg-dark text-light p-3 rounded"><code>curl -X POST -H 'Content-Type:application/json' http://localhost:8080/schedules -d @sample.json</code></pre>
657
+
658
+ <h3 class="mt-4">3. Check status</h3>
659
+ <pre class="bg-dark text-light p-3 rounded"><code>curl -X GET http://localhost:8080/schedules/{jobId}/status</code></pre>
660
+
661
+ <h3 class="mt-4">4. Get solution</h3>
662
+ <pre class="bg-dark text-light p-3 rounded"><code>curl -X GET http://localhost:8080/schedules/{jobId}</code></pre>
663
+
664
+ <h3 class="mt-4">5. Stop solving</h3>
665
+ <pre class="bg-dark text-light p-3 rounded"><code>curl -X DELETE http://localhost:8080/schedules/{jobId}</code></pre>
666
+ </div>
667
+ </div>
668
+
669
+ <!--
670
+ ===========================================================================
671
+ API TAB (SWAGGER UI)
672
+ ===========================================================================
673
+
674
+ Embeds the FastAPI auto-generated Swagger UI documentation.
675
+
676
+ The iframe loads /q/swagger-ui which is served by the FastAPI backend.
677
+ This provides interactive API documentation and testing.
678
+
679
+ NOTE: The /q/ prefix is the Quarkus-style path used by FastAPI with
680
+ the swagger-ui extension.
681
+ ===========================================================================
682
+ -->
683
+ <div class="tab-pane fade" id="api">
684
+ <h1 class="mb-4"><i class="fas fa-plug me-2"></i>REST API Reference</h1>
685
+ <div class="ratio ratio-1x1" style="max-height: 800px;">
686
+ <iframe src="/q/swagger-ui" style="border-radius: 12px;"></iframe>
687
+ </div>
688
+ </div>
689
+
690
+ </div>
691
+
692
+ <!--
693
+ ================================================================================
694
+ SCORE ANALYSIS MODAL
695
+ ================================================================================
696
+
697
+ Modal dialog showing detailed score breakdown from /schedules/analyze endpoint.
698
+
699
+ Displayed when user clicks the Analyze button.
700
+ Content populated by analyze() function in app.js.
701
+
702
+ Shows:
703
+ - Overall score
704
+ - Constraint match details (which constraints are violated and by how much)
705
+ ================================================================================
706
+ -->
707
+ <div class="modal fade" id="scoreAnalysisModal" tabindex="-1">
708
+ <div class="modal-dialog modal-lg modal-dialog-scrollable">
709
+ <div class="modal-content">
710
+ <div class="modal-header">
711
+ <h5 class="modal-title">
712
+ <i class="fas fa-microscope me-2"></i>Score Analysis
713
+ <span id="scoreAnalysisScore" class="text-muted ms-2"></span>
714
+ </h5>
715
+ <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
716
+ </div>
717
+ <div class="modal-body" id="scoreAnalysisContent">
718
+ <!-- Populated by analyze() in app.js -->
719
+ </div>
720
+ <div class="modal-footer">
721
+ <button type="button" class="btn btn-primary" data-bs-dismiss="modal">Close</button>
722
+ </div>
723
+ </div>
724
+ </div>
725
+ </div>
726
+
727
+ <!--
728
+ ================================================================================
729
+ ADD RESOURCE MODAL
730
+ ================================================================================
731
+ -->
732
+ <div class="modal fade" id="addResourceModal" tabindex="-1">
733
+ <div class="modal-dialog">
734
+ <div class="modal-content">
735
+ <div class="modal-header">
736
+ <h5 class="modal-title">
737
+ <i class="fas fa-user-plus me-2"></i>Add Resource
738
+ </h5>
739
+ <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
740
+ </div>
741
+ <div class="modal-body">
742
+ <div class="mb-3">
743
+ <label for="resourceName" class="form-label">Name</label>
744
+ <input type="text" class="form-control" id="resourceName" placeholder="e.g., Alice">
745
+ </div>
746
+ <div class="mb-3">
747
+ <label for="resourceCapacity" class="form-label">Capacity (minutes)</label>
748
+ <input type="number" class="form-control" id="resourceCapacity" value="100" min="1">
749
+ </div>
750
+ <div class="mb-3">
751
+ <label for="resourceSkills" class="form-label">Skills (comma-separated)</label>
752
+ <input type="text" class="form-control" id="resourceSkills" placeholder="e.g., python, sql, java">
753
+ </div>
754
+ </div>
755
+ <div class="modal-footer">
756
+ <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
757
+ <button type="button" class="btn btn-success" onclick="addResource()">
758
+ <i class="fas fa-plus me-1"></i>Add Resource
759
+ </button>
760
+ </div>
761
+ </div>
762
+ </div>
763
+ </div>
764
+
765
+ <!--
766
+ ================================================================================
767
+ ADD TASK MODAL
768
+ ================================================================================
769
+ -->
770
+ <div class="modal fade" id="addTaskModal" tabindex="-1">
771
+ <div class="modal-dialog">
772
+ <div class="modal-content">
773
+ <div class="modal-header">
774
+ <h5 class="modal-title">
775
+ <i class="fas fa-tasks me-2"></i>Add Task
776
+ </h5>
777
+ <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
778
+ </div>
779
+ <div class="modal-body">
780
+ <div class="mb-3">
781
+ <label for="taskName" class="form-label">Name</label>
782
+ <input type="text" class="form-control" id="taskName" placeholder="e.g., API Development">
783
+ </div>
784
+ <div class="mb-3">
785
+ <label for="taskDuration" class="form-label">Duration (minutes)</label>
786
+ <input type="number" class="form-control" id="taskDuration" value="30" min="1">
787
+ </div>
788
+ <div class="mb-3">
789
+ <label for="taskSkill" class="form-label">Required Skill (optional)</label>
790
+ <input type="text" class="form-control" id="taskSkill" placeholder="e.g., python">
791
+ </div>
792
+ </div>
793
+ <div class="modal-footer">
794
+ <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
795
+ <button type="button" class="btn btn-success" onclick="addTask()">
796
+ <i class="fas fa-plus me-1"></i>Add Task
797
+ </button>
798
+ </div>
799
+ </div>
800
+ </div>
801
+ </div>
802
+
803
+ <!--
804
+ ================================================================================
805
+ JAVASCRIPT DEPENDENCIES
806
+ ================================================================================
807
+
808
+ Required libraries (loaded from CDN):
809
+ - jQuery 3.7.1: DOM manipulation and AJAX
810
+ - Popper.js 2.11.8: Bootstrap positioning engine
811
+ - Bootstrap 5.3.3: UI components (modals, tabs, dropdowns)
812
+ - Prism.js 1.29.0: Syntax highlighting for code viewer
813
+ - Core library
814
+ - Python language support
815
+ - JavaScript language support
816
+ - Line numbers plugin
817
+
818
+ LOAD ORDER MATTERS:
819
+ 1. jQuery (required by Bootstrap)
820
+ 2. Popper.js (required by Bootstrap)
821
+ 3. Bootstrap
822
+ 4. Prism.js (core, then language modules, then plugins)
823
+ 5. app.js (our custom code)
824
+
825
+ TIP: For production, consider bundling these locally.
826
+ ================================================================================
827
+ -->
828
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
829
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/2.11.8/umd/popper.min.js"></script>
830
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.3/js/bootstrap.min.js"></script>
831
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js"></script>
832
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-python.min.js"></script>
833
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-javascript.min.js"></script>
834
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/plugins/line-numbers/prism-line-numbers.min.js"></script>
835
+
836
+ <!--
837
+ ================================================================================
838
+ APPLICATION JAVASCRIPT
839
+ ================================================================================
840
+
841
+ The main application logic is in app.js. It handles:
842
+ - Loading demo data from the API
843
+ - Starting/stopping the solver
844
+ - Polling for solution updates
845
+ - Rendering the solution visualization
846
+ - Learn tab lesson navigation
847
+ - Build tab code viewer
848
+ - Interactive Code click-to-code feature
849
+
850
+ See app.js for detailed documentation of each function.
851
+ ================================================================================
852
+ -->
853
+ <script src="/app.js"></script>
854
+
855
+ </body>
856
+ </html>
static/webjars/solverforge/css/solverforge-webui.css ADDED
@@ -0,0 +1,122 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /*
2
+ * =============================================================================
3
+ * SOLVERFORGE QUICKSTART - WEB UI STYLESHEET
4
+ * =============================================================================
5
+ *
6
+ * This CSS file provides the base styling for all SolverForge quickstart UIs.
7
+ * It customizes Bootstrap 5 variables and adds SolverForge-specific styles.
8
+ *
9
+ * CUSTOMIZATION GUIDE:
10
+ * --------------------
11
+ * 1. Colors: Modify the CSS variables in :root to match your branding
12
+ * 2. Buttons: The .btn classes customize Bootstrap button styles
13
+ * 3. Navigation: .nav-pills styles the main navigation tabs
14
+ * 4. Layout: The navbar height is defined for consistent positioning
15
+ *
16
+ * The main colors used are:
17
+ * - Green (#10b981): Primary action color, success states
18
+ * - Violet (#3E00FF): Secondary accent, links
19
+ * - Gray (#666666): Text, borders
20
+ * - Light (#F2F2F2): Background
21
+ */
22
+
23
+ :root {
24
+ /* =======================================================================
25
+ * NAVBAR & LAYOUT
26
+ * Keep in sync with .navbar height on a large screen.
27
+ * ======================================================================= */
28
+ --ts-navbar-height: 109px;
29
+
30
+ /* =======================================================================
31
+ * COLOR PALETTE
32
+ * These are the SolverForge brand colors. Modify to match your branding.
33
+ * ======================================================================= */
34
+ --ts-green-1-rgb: #10b981; /* Primary green - used for success, primary actions */
35
+ --ts-green-2-rgb: #059669; /* Darker green - used for hover states */
36
+ --ts-violet-1-rgb: #3E00FF; /* Primary violet - used for links, accents */
37
+ --ts-violet-2-rgb: #3423A6; /* Secondary violet - used for hover states */
38
+ --ts-violet-3-rgb: #2E1760; /* Dark violet - used for dark backgrounds */
39
+ --ts-violet-4-rgb: #200F4F; /* Darker violet */
40
+ --ts-violet-5-rgb: #000000; /* Darkest (currently black) */
41
+ --ts-violet-dark-1-rgb: #b6adfd; /* Light violet for dark backgrounds */
42
+ --ts-violet-dark-2-rgb: #c1bbfd; /* Lighter violet for dark backgrounds */
43
+ --ts-gray-rgb: #666666; /* Gray text */
44
+ --ts-white-rgb: #FFFFFF; /* White */
45
+ --ts-light-rgb: #F2F2F2; /* Light background */
46
+ --ts-gray-border: #c5c5c5; /* Border color */
47
+
48
+ /* =======================================================================
49
+ * BOOTSTRAP OVERRIDES
50
+ * These override Bootstrap's default CSS variables.
51
+ * ======================================================================= */
52
+ --tf-light-rgb-transparent: rgb(242,242,242,0.5);
53
+ --bs-body-bg: var(--ts-light-rgb);
54
+ --bs-link-color: var(--ts-violet-1-rgb);
55
+ --bs-link-hover-color: var(--ts-violet-2-rgb);
56
+
57
+ --bs-navbar-color: var(--ts-white-rgb);
58
+ --bs-navbar-hover-color: var(--ts-white-rgb);
59
+ --bs-nav-link-font-size: 18px;
60
+ --bs-nav-link-font-weight: 400;
61
+ --bs-nav-link-color: var(--ts-white-rgb);
62
+ --ts-nav-link-hover-border-color: var(--ts-violet-1-rgb);
63
+ }
64
+
65
+ /* ===========================================================================
66
+ * BUTTON STYLES
67
+ * Customizes Bootstrap buttons with rounded corners and SolverForge colors.
68
+ * =========================================================================== */
69
+ .btn {
70
+ --bs-btn-border-radius: 1.5rem;
71
+ }
72
+
73
+ /* Primary button - violet color */
74
+ .btn-primary {
75
+ --bs-btn-bg: var(--ts-violet-1-rgb);
76
+ --bs-btn-border-color: var(--ts-violet-1-rgb);
77
+ --bs-btn-hover-bg: var(--ts-violet-2-rgb);
78
+ --bs-btn-hover-border-color: var(--ts-violet-2-rgb);
79
+ --bs-btn-active-bg: var(--ts-violet-2-rgb);
80
+ --bs-btn-active-border-bg: var(--ts-violet-2-rgb);
81
+ --bs-btn-disabled-bg: var(--ts-violet-1-rgb);
82
+ --bs-btn-disabled-border-color: var(--ts-violet-1-rgb);
83
+ }
84
+
85
+ /* Outline primary button */
86
+ .btn-outline-primary {
87
+ --bs-btn-color: var(--ts-violet-1-rgb);
88
+ --bs-btn-border-color: var(--ts-violet-1-rgb);
89
+ --bs-btn-hover-bg: var(--ts-violet-1-rgb);
90
+ --bs-btn-hover-border-color: var(--ts-violet-1-rgb);
91
+ --bs-btn-active-bg: var(--ts-violet-1-rgb);
92
+ --bs-btn-active-border-color: var(--ts-violet-1-rgb);
93
+ --bs-btn-disabled-color: var(--ts-violet-1-rgb);
94
+ --bs-btn-disabled-border-color: var(--ts-violet-1-rgb);
95
+ }
96
+
97
+ /* ===========================================================================
98
+ * NAVBAR STYLES
99
+ * Styles for the dark navbar variant.
100
+ * =========================================================================== */
101
+ .navbar-dark {
102
+ --bs-link-color: var(--ts-violet-dark-1-rgb);
103
+ --bs-link-hover-color: var(--ts-violet-dark-2-rgb);
104
+ --bs-navbar-color: var(--ts-white-rgb);
105
+ --bs-navbar-hover-color: var(--ts-white-rgb);
106
+ }
107
+
108
+ /* ===========================================================================
109
+ * NAVIGATION PILLS
110
+ * Styles for the main tab navigation.
111
+ * =========================================================================== */
112
+ .nav-pills {
113
+ --bs-nav-pills-link-active-bg: var(--ts-green-1-rgb);
114
+ }
115
+
116
+ .nav-pills .nav-link:hover {
117
+ color: var(--ts-green-1-rgb);
118
+ }
119
+
120
+ .nav-pills .nav-link.active:hover {
121
+ color: var(--ts-white-rgb);
122
+ }
static/webjars/solverforge/img/solverforge-favicon.svg ADDED
static/webjars/solverforge/img/solverforge-horizontal-white.svg ADDED
static/webjars/solverforge/img/solverforge-horizontal.svg ADDED
static/webjars/solverforge/img/solverforge-logo-stacked.svg ADDED
static/webjars/solverforge/js/solverforge-webui.js ADDED
@@ -0,0 +1,333 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /*
2
+ * =============================================================================
3
+ * SOLVERFORGE QUICKSTART - SHARED WEB UI UTILITIES
4
+ * =============================================================================
5
+ *
6
+ * This file provides shared utility functions for all SolverForge quickstart UIs.
7
+ * It includes:
8
+ * - Header/footer generation
9
+ * - Error notification handling
10
+ * - Application info display
11
+ * - Color utilities for visualizations
12
+ *
13
+ * USAGE:
14
+ * ------
15
+ * 1. Include this file AFTER jQuery and Bootstrap in your HTML
16
+ * 2. Call replaceQuickstartSolverForgeAutoHeaderFooter() in your app.js
17
+ * 3. Use showError() for displaying error notifications
18
+ * 4. Use pickColor()/nextColor() for consistent visualization colors
19
+ */
20
+
21
+
22
+ // =============================================================================
23
+ // HEADER & FOOTER GENERATION
24
+ // =============================================================================
25
+
26
+ /**
27
+ * Replaces the placeholder header and footer with the SolverForge branded versions.
28
+ *
29
+ * This function looks for:
30
+ * - <header id="solverforge-auto-header"> - replaced with navbar
31
+ * - <footer id="solverforge-auto-footer"> - replaced with footer links
32
+ *
33
+ * The header includes:
34
+ * - SolverForge logo linking to homepage
35
+ * - Navigation tabs: Demo UI, Guide, REST API
36
+ * - Data dropdown for selecting demo datasets
37
+ *
38
+ * CUSTOMIZATION:
39
+ * - Modify the HTML template below to change navigation items
40
+ * - Update logo src to use a different logo
41
+ * - Add/remove nav items as needed
42
+ */
43
+ function replaceQuickstartSolverForgeAutoHeaderFooter() {
44
+ const solverforgeHeader = $("header#solverforge-auto-header");
45
+ if (solverforgeHeader != null) {
46
+ // Set white background for header
47
+ solverforgeHeader.css("background-color", "#ffffff");
48
+
49
+ // Append the navbar HTML
50
+ solverforgeHeader.append(
51
+ $(`<div class="container-fluid">
52
+ <nav class="navbar sticky-top navbar-expand-lg shadow-sm mb-3" style="background-color: #ffffff;">
53
+ <!-- Logo - links to SolverForge homepage -->
54
+ <a class="navbar-brand" href="https://www.solverforge.org">
55
+ <img src="/webjars/solverforge/img/solverforge-horizontal.svg" alt="SolverForge logo" width="400">
56
+ </a>
57
+
58
+ <!-- Mobile toggle button -->
59
+ <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
60
+ <span class="navbar-toggler-icon"></span>
61
+ </button>
62
+
63
+ <!-- Navigation items -->
64
+ <div class="collapse navbar-collapse" id="navbarNav">
65
+ <ul class="nav nav-pills">
66
+ <!-- Demo UI tab - shows the main visualization -->
67
+ <li class="nav-item active" id="navUIItem">
68
+ <button class="nav-link active" id="navUI" data-bs-toggle="pill" data-bs-target="#demo" type="button" style="color: #1f2937;">Demo UI</button>
69
+ </li>
70
+ <!-- Guide tab - shows REST API usage guide -->
71
+ <li class="nav-item" id="navRestItem">
72
+ <button class="nav-link" id="navRest" data-bs-toggle="pill" data-bs-target="#rest" type="button" style="color: #1f2937;">Guide</button>
73
+ </li>
74
+ <!-- REST API tab - shows Swagger/OpenAPI docs -->
75
+ <li class="nav-item" id="navOpenApiItem">
76
+ <button class="nav-link" id="navOpenApi" data-bs-toggle="pill" data-bs-target="#openapi" type="button" style="color: #1f2937;">REST API</button>
77
+ </li>
78
+ </ul>
79
+ </div>
80
+
81
+ <!-- Data dropdown - populated dynamically with demo datasets -->
82
+ <div class="ms-auto">
83
+ <div class="dropdown">
84
+ <button class="btn dropdown-toggle" type="button" id="dropdownMenuButton" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false" style="background-color: #10b981; color: #ffffff; border-color: #10b981;">
85
+ Data
86
+ </button>
87
+ <div id="testDataButton" class="dropdown-menu" aria-labelledby="dropdownMenuButton">
88
+ <!-- Demo data items will be added here by fetchDemoData() -->
89
+ </div>
90
+ </div>
91
+ </div>
92
+ </nav>
93
+ </div>`));
94
+ }
95
+
96
+ const solverforgeFooter = $("footer#solverforge-auto-footer");
97
+ if (solverforgeFooter != null) {
98
+ // Append the footer HTML
99
+ solverforgeFooter.append(
100
+ $(`<footer class="bg-black text-white-50">
101
+ <div class="container">
102
+ <div class="hstack gap-3 p-4">
103
+ <div class="ms-auto"><a class="text-white" href="https://www.solverforge.org">SolverForge</a></div>
104
+ <div class="vr"></div>
105
+ <div><a class="text-white" href="https://www.solverforge.org/docs">Documentation</a></div>
106
+ <div class="vr"></div>
107
+ <div><a class="text-white" href="https://github.com/SolverForge/solverforge">Code</a></div>
108
+ <div class="vr"></div>
109
+ <div class="me-auto"><a class="text-white" href="mailto:[email protected]">Support</a></div>
110
+ </div>
111
+ </div>
112
+ <!-- Application info will be displayed here -->
113
+ <div id="applicationInfo" class="container text-center"></div>
114
+ </footer>`));
115
+
116
+ // Load and display application info
117
+ applicationInfo();
118
+ }
119
+ }
120
+
121
+
122
+ // =============================================================================
123
+ // ERROR NOTIFICATION HANDLING
124
+ // =============================================================================
125
+
126
+ /**
127
+ * Shows a simple error notification with just a title.
128
+ *
129
+ * Use this for simple error messages that don't have additional details.
130
+ * The notification auto-dismisses after 30 seconds.
131
+ *
132
+ * @param {string} title - The error message to display
133
+ *
134
+ * EXAMPLE:
135
+ * showSimpleError("No schedule data available");
136
+ */
137
+ function showSimpleError(title) {
138
+ const notification = $(`<div class="toast" role="alert" aria-live="assertive" aria-atomic="true" style="min-width: 50rem"/>`)
139
+ .append($(`<div class="toast-header bg-danger">
140
+ <strong class="me-auto text-dark">Error</strong>
141
+ <button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
142
+ </div>`))
143
+ .append($(`<div class="toast-body"/>`)
144
+ .append($(`<p/>`).text(title))
145
+ );
146
+ $("#notificationPanel").append(notification);
147
+ notification.toast({delay: 30000});
148
+ notification.toast('show');
149
+ }
150
+
151
+ /**
152
+ * Shows a detailed error notification from an AJAX error response.
153
+ *
154
+ * This extracts error details from the XMLHttpRequest object and displays:
155
+ * - Error title (from the caller)
156
+ * - Server error message
157
+ * - Error code
158
+ * - Error ID (for debugging)
159
+ *
160
+ * @param {string} title - A human-readable title for the error
161
+ * @param {XMLHttpRequest} xhr - The jQuery AJAX error object
162
+ *
163
+ * EXAMPLE:
164
+ * $.post("/schedules", data).fail(function(xhr) {
165
+ * showError("Failed to start solving", xhr);
166
+ * });
167
+ */
168
+ function showError(title, xhr) {
169
+ // Extract error details from response
170
+ var serverErrorMessage = !xhr.responseJSON ? `${xhr.status}: ${xhr.statusText}` : xhr.responseJSON.message;
171
+ var serverErrorCode = !xhr.responseJSON ? `unknown` : xhr.responseJSON.code;
172
+ var serverErrorId = !xhr.responseJSON ? `----` : xhr.responseJSON.id;
173
+ var serverErrorDetails = !xhr.responseJSON ? `no details provided` : xhr.responseJSON.details;
174
+
175
+ // Handle case where responseJSON exists but has unexpected format
176
+ if (xhr.responseJSON && !serverErrorMessage) {
177
+ serverErrorMessage = JSON.stringify(xhr.responseJSON);
178
+ serverErrorCode = xhr.statusText + '(' + xhr.status + ')';
179
+ serverErrorId = `----`;
180
+ }
181
+
182
+ // Log to console for debugging
183
+ console.error(title + "\n" + serverErrorMessage + " : " + serverErrorDetails);
184
+
185
+ // Create and show toast notification
186
+ const notification = $(`<div class="toast" role="alert" aria-live="assertive" aria-atomic="true" style="min-width: 50rem"/>`)
187
+ .append($(`<div class="toast-header bg-danger">
188
+ <strong class="me-auto text-dark">Error</strong>
189
+ <button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
190
+ </div>`))
191
+ .append($(`<div class="toast-body"/>`)
192
+ .append($(`<p/>`).text(title))
193
+ .append($(`<pre/>`)
194
+ .append($(`<code/>`).text(serverErrorMessage + "\n\nCode: " + serverErrorCode + "\nError id: " + serverErrorId))
195
+ )
196
+ );
197
+ $("#notificationPanel").append(notification);
198
+ notification.toast({delay: 30000});
199
+ notification.toast('show');
200
+ }
201
+
202
+
203
+ // =============================================================================
204
+ // APPLICATION INFO
205
+ // =============================================================================
206
+
207
+ /**
208
+ * Fetches and displays application version information in the footer.
209
+ *
210
+ * This calls the /info endpoint (if available) and displays:
211
+ * - Application name
212
+ * - Version number
213
+ * - Build timestamp
214
+ *
215
+ * NOTE: Your backend must implement the /info endpoint for this to work.
216
+ * If not implemented, the footer will just show the links without version info.
217
+ */
218
+ function applicationInfo() {
219
+ $.getJSON("info", function (info) {
220
+ $("#applicationInfo").append("<small>" + info.application + " (version: " + info.version + ", built at: " + info.built + ")</small>");
221
+ }).fail(function (xhr, ajaxOptions, thrownError) {
222
+ console.warn("Unable to collect application information");
223
+ });
224
+ }
225
+
226
+
227
+ // =============================================================================
228
+ // TANGO COLOR FACTORY
229
+ // =============================================================================
230
+ //
231
+ // These functions provide a consistent color palette for visualizations.
232
+ // The colors are based on the Tango color palette, which provides good
233
+ // contrast and accessibility.
234
+ //
235
+ // USAGE:
236
+ // let color = pickColor("employee-1"); // Returns consistent color for same object
237
+ // let color = nextColor(); // Returns next color in sequence
238
+
239
+ const SEQUENCE_1 = [0x8AE234, 0xFCE94F, 0x729FCF, 0xE9B96E, 0xAD7FA8];
240
+ const SEQUENCE_2 = [0x73D216, 0xEDD400, 0x3465A4, 0xC17D11, 0x75507B];
241
+
242
+ var colorMap = new Map;
243
+ var nextColorCount = 0;
244
+
245
+ /**
246
+ * Returns a consistent color for an object.
247
+ *
248
+ * If the object has been seen before, returns the same color.
249
+ * Otherwise, assigns and returns a new color.
250
+ *
251
+ * @param {*} object - Any object/value to get a color for
252
+ * @returns {string} A hex color string (e.g., "#8ae234")
253
+ */
254
+ function pickColor(object) {
255
+ let color = colorMap[object];
256
+ if (color !== undefined) {
257
+ return color;
258
+ }
259
+ color = nextColor();
260
+ colorMap[object] = color;
261
+ return color;
262
+ }
263
+
264
+ /**
265
+ * Returns the next color in the sequence.
266
+ *
267
+ * Cycles through SEQUENCE_1, then SEQUENCE_2, then generates
268
+ * interpolated colors for additional objects.
269
+ *
270
+ * @returns {string} A hex color string (e.g., "#8ae234")
271
+ */
272
+ function nextColor() {
273
+ let color;
274
+ let colorIndex = nextColorCount % SEQUENCE_1.length;
275
+ let shadeIndex = Math.floor(nextColorCount / SEQUENCE_1.length);
276
+
277
+ if (shadeIndex === 0) {
278
+ color = SEQUENCE_1[colorIndex];
279
+ } else if (shadeIndex === 1) {
280
+ color = SEQUENCE_2[colorIndex];
281
+ } else {
282
+ // Generate interpolated colors for additional items
283
+ shadeIndex -= 3;
284
+ let floorColor = SEQUENCE_2[colorIndex];
285
+ let ceilColor = SEQUENCE_1[colorIndex];
286
+ let base = Math.floor((shadeIndex / 2) + 1);
287
+ let divisor = 2;
288
+ while (base >= divisor) {
289
+ divisor *= 2;
290
+ }
291
+ base = (base * 2) - divisor + 1;
292
+ let shadePercentage = base / divisor;
293
+ color = buildPercentageColor(floorColor, ceilColor, shadePercentage);
294
+ }
295
+ nextColorCount++;
296
+ return "#" + color.toString(16);
297
+ }
298
+
299
+ /**
300
+ * Interpolates between two colors.
301
+ *
302
+ * @param {number} floorColor - The starting color (as integer)
303
+ * @param {number} ceilColor - The ending color (as integer)
304
+ * @param {number} shadePercentage - How far to interpolate (0-1)
305
+ * @returns {number} The interpolated color (as integer)
306
+ */
307
+ function buildPercentageColor(floorColor, ceilColor, shadePercentage) {
308
+ let red = (floorColor & 0xFF0000) + Math.floor(shadePercentage * ((ceilColor & 0xFF0000) - (floorColor & 0xFF0000))) & 0xFF0000;
309
+ let green = (floorColor & 0x00FF00) + Math.floor(shadePercentage * ((ceilColor & 0x00FF00) - (floorColor & 0x00FF00))) & 0x00FF00;
310
+ let blue = (floorColor & 0x0000FF) + Math.floor(shadePercentage * ((ceilColor & 0x0000FF) - (floorColor & 0x0000FF))) & 0x0000FF;
311
+ return red | green | blue;
312
+ }
313
+
314
+
315
+ // =============================================================================
316
+ // CLIPBOARD UTILITY
317
+ // =============================================================================
318
+
319
+ /**
320
+ * Copies text content to the clipboard.
321
+ *
322
+ * Used by the Guide tab to let users copy cURL commands.
323
+ *
324
+ * @param {string} elementId - The ID of the element containing text to copy
325
+ */
326
+ function copyTextToClipboard(elementId) {
327
+ const text = document.getElementById(elementId).textContent;
328
+ navigator.clipboard.writeText(text).then(function() {
329
+ console.log('Copied to clipboard: ' + text.substring(0, 50) + '...');
330
+ }).catch(function(err) {
331
+ console.error('Failed to copy text: ', err);
332
+ });
333
+ }
tests/__pycache__/test_constraints.cpython-313-pytest-9.0.2.pyc ADDED
Binary file (6.74 kB). View file
 
tests/__pycache__/test_rest_api.cpython-313-pytest-9.0.2.pyc ADDED
Binary file (14.2 kB). View file
 
tests/test_constraints.py ADDED
@@ -0,0 +1,140 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Constraint verification tests.
3
+
4
+ These tests verify that each constraint behaves correctly in isolation.
5
+ Use ConstraintVerifier to test individual constraints without running the full solver.
6
+ """
7
+
8
+ import pytest
9
+ from solverforge_legacy.solver.test import ConstraintVerifier
10
+ from my_quickstart.domain import Resource, Task, Schedule
11
+ from my_quickstart.constraints import define_constraints
12
+
13
+
14
+ @pytest.fixture
15
+ def constraint_verifier():
16
+ """Create a constraint verifier for testing."""
17
+ return ConstraintVerifier.build(
18
+ define_constraints,
19
+ Schedule,
20
+ Task,
21
+ )
22
+
23
+
24
+ # =============================================================================
25
+ # TEST DATA FIXTURES
26
+ # =============================================================================
27
+
28
+ @pytest.fixture
29
+ def alice():
30
+ return Resource(name="Alice", capacity=100, skills={"python", "sql"})
31
+
32
+
33
+ @pytest.fixture
34
+ def bob():
35
+ return Resource(name="Bob", capacity=50, skills={"java"})
36
+
37
+
38
+ # =============================================================================
39
+ # HARD CONSTRAINT TESTS
40
+ # =============================================================================
41
+
42
+ class TestRequiredSkill:
43
+ """Tests for the 'Required skill missing' constraint."""
44
+
45
+ def test_no_penalty_when_skill_matches(self, constraint_verifier, alice):
46
+ """Task with matching skill should not be penalized."""
47
+ task = Task(id="1", name="Python Task", duration=30, required_skill="python", resource=alice)
48
+
49
+ constraint_verifier.verify_that("Required skill missing") \
50
+ .given(task) \
51
+ .penalizes_by(0)
52
+
53
+ def test_penalty_when_skill_missing(self, constraint_verifier, bob):
54
+ """Task assigned to resource without required skill should be penalized."""
55
+ task = Task(id="1", name="Python Task", duration=30, required_skill="python", resource=bob)
56
+
57
+ constraint_verifier.verify_that("Required skill missing") \
58
+ .given(task) \
59
+ .penalizes_by(1)
60
+
61
+ def test_no_penalty_when_no_skill_required(self, constraint_verifier, alice):
62
+ """Task with no skill requirement should not be penalized."""
63
+ task = Task(id="1", name="Any Task", duration=30, required_skill="", resource=alice)
64
+
65
+ constraint_verifier.verify_that("Required skill missing") \
66
+ .given(task) \
67
+ .penalizes_by(0)
68
+
69
+
70
+ class TestResourceCapacity:
71
+ """Tests for the 'Resource capacity exceeded' constraint."""
72
+
73
+ def test_no_penalty_under_capacity(self, constraint_verifier, alice):
74
+ """Tasks under capacity should not be penalized."""
75
+ task1 = Task(id="1", name="Task 1", duration=30, resource=alice)
76
+ task2 = Task(id="2", name="Task 2", duration=40, resource=alice)
77
+ # Total: 70, Capacity: 100
78
+
79
+ constraint_verifier.verify_that("Resource capacity exceeded") \
80
+ .given(task1, task2) \
81
+ .penalizes_by(0)
82
+
83
+ def test_penalty_over_capacity(self, constraint_verifier, bob):
84
+ """Tasks exceeding capacity should be penalized by the overflow amount."""
85
+ task1 = Task(id="1", name="Task 1", duration=30, resource=bob)
86
+ task2 = Task(id="2", name="Task 2", duration=40, resource=bob)
87
+ # Total: 70, Capacity: 50, Overflow: 20
88
+
89
+ constraint_verifier.verify_that("Resource capacity exceeded") \
90
+ .given(task1, task2) \
91
+ .penalizes_by(20)
92
+
93
+
94
+ # =============================================================================
95
+ # SOFT CONSTRAINT TESTS
96
+ # =============================================================================
97
+
98
+ class TestMinimizeDuration:
99
+ """Tests for the 'Minimize total duration' constraint."""
100
+
101
+ def test_penalizes_by_duration(self, constraint_verifier, alice):
102
+ """Each assigned task should be penalized by its duration."""
103
+ task = Task(id="1", name="Task", duration=45, resource=alice)
104
+
105
+ constraint_verifier.verify_that("Minimize total duration") \
106
+ .given(task) \
107
+ .penalizes_by(45)
108
+
109
+ def test_unassigned_not_penalized(self, constraint_verifier):
110
+ """Unassigned tasks should not be penalized."""
111
+ task = Task(id="1", name="Task", duration=45, resource=None)
112
+
113
+ constraint_verifier.verify_that("Minimize total duration") \
114
+ .given(task) \
115
+ .penalizes_by(0)
116
+
117
+
118
+ # =============================================================================
119
+ # INTEGRATION TEST
120
+ # =============================================================================
121
+
122
+ class TestFullSolution:
123
+ """Test the full constraint set on a complete solution."""
124
+
125
+ def test_feasible_solution(self, constraint_verifier, alice, bob):
126
+ """A feasible solution should have no hard constraint violations."""
127
+ tasks = [
128
+ Task(id="1", name="Python Task", duration=30, required_skill="python", resource=alice),
129
+ Task(id="2", name="SQL Task", duration=20, required_skill="sql", resource=alice),
130
+ Task(id="3", name="Java Task", duration=40, required_skill="java", resource=bob),
131
+ ]
132
+
133
+ # Verify no hard violations
134
+ constraint_verifier.verify_that("Required skill missing") \
135
+ .given(*tasks) \
136
+ .penalizes_by(0)
137
+
138
+ constraint_verifier.verify_that("Resource capacity exceeded") \
139
+ .given(*tasks) \
140
+ .penalizes_by(0)
tests/test_rest_api.py ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ REST API endpoint tests.
3
+
4
+ Tests for the FastAPI endpoints including the source code viewer API.
5
+ """
6
+
7
+ import pytest
8
+ from fastapi.testclient import TestClient
9
+ from my_quickstart.rest_api import app
10
+
11
+
12
+ @pytest.fixture
13
+ def client():
14
+ """Create a test client for the API."""
15
+ return TestClient(app)
16
+
17
+
18
+ # =============================================================================
19
+ # SOURCE CODE VIEWER TESTS
20
+ # =============================================================================
21
+
22
+ class TestSourceCodeEndpoints:
23
+ """Tests for the /source-code endpoints."""
24
+
25
+ def test_list_source_files(self, client):
26
+ """GET /source-code should return list of available files."""
27
+ response = client.get("/source-code")
28
+ assert response.status_code == 200
29
+ files = response.json()
30
+ assert isinstance(files, list)
31
+ assert "domain.py" in files
32
+ assert "constraints.py" in files
33
+ assert "rest_api.py" in files
34
+
35
+ def test_get_domain_py(self, client):
36
+ """GET /source-code/domain.py should return file contents."""
37
+ response = client.get("/source-code/domain.py")
38
+ assert response.status_code == 200
39
+ data = response.json()
40
+ assert "filename" in data
41
+ assert "content" in data
42
+ assert data["filename"] == "domain.py"
43
+ assert "@planning_entity" in data["content"]
44
+
45
+ def test_get_constraints_py(self, client):
46
+ """GET /source-code/constraints.py should return file contents."""
47
+ response = client.get("/source-code/constraints.py")
48
+ assert response.status_code == 200
49
+ data = response.json()
50
+ assert "content" in data
51
+ assert "@constraint_provider" in data["content"]
52
+
53
+ def test_get_nonexistent_file(self, client):
54
+ """GET /source-code/nonexistent.py should return error."""
55
+ response = client.get("/source-code/nonexistent.py")
56
+ assert response.status_code != 200
57
+
58
+
59
+ # =============================================================================
60
+ # DEMO DATA TESTS
61
+ # =============================================================================
62
+
63
+ class TestDemoDataEndpoints:
64
+ """Tests for the /demo-data endpoints."""
65
+
66
+ def test_list_demo_data(self, client):
67
+ """GET /demo-data should return list of datasets."""
68
+ response = client.get("/demo-data")
69
+ assert response.status_code == 200
70
+ datasets = response.json()
71
+ assert isinstance(datasets, list)
72
+
73
+ def test_get_small_dataset(self, client):
74
+ """GET /demo-data/SMALL should return a schedule."""
75
+ response = client.get("/demo-data/SMALL")
76
+ assert response.status_code == 200
77
+ data = response.json()
78
+ assert "resources" in data
79
+ assert "tasks" in data