Spaces:
Sleeping
Sleeping
Upload 33 files
Browse files- .gitignore +44 -0
- Dockerfile +33 -0
- LICENSE +201 -0
- README_HUGGINGFACE.md +47 -0
- logging.conf +30 -0
- pyproject.toml +24 -0
- src/my_quickstart/__init__.py +21 -0
- src/my_quickstart/__pycache__/__init__.cpython-312.pyc +0 -0
- src/my_quickstart/__pycache__/constraints.cpython-312.pyc +0 -0
- src/my_quickstart/__pycache__/demo_data.cpython-312.pyc +0 -0
- src/my_quickstart/__pycache__/domain.cpython-312.pyc +0 -0
- src/my_quickstart/__pycache__/json_serialization.cpython-312.pyc +0 -0
- src/my_quickstart/__pycache__/rest_api.cpython-312.pyc +0 -0
- src/my_quickstart/__pycache__/solver.cpython-312.pyc +0 -0
- src/my_quickstart/constraints.py +239 -0
- src/my_quickstart/demo_data.py +84 -0
- src/my_quickstart/domain.py +141 -0
- src/my_quickstart/json_serialization.py +42 -0
- src/my_quickstart/rest_api.py +285 -0
- src/my_quickstart/solver.py +40 -0
- static/app.css +728 -0
- static/app.js +1836 -0
- static/index.html +856 -0
- static/webjars/solverforge/css/solverforge-webui.css +122 -0
- static/webjars/solverforge/img/solverforge-favicon.svg +65 -0
- static/webjars/solverforge/img/solverforge-horizontal-white.svg +66 -0
- static/webjars/solverforge/img/solverforge-horizontal.svg +65 -0
- static/webjars/solverforge/img/solverforge-logo-stacked.svg +73 -0
- static/webjars/solverforge/js/solverforge-webui.js +333 -0
- tests/__pycache__/test_constraints.cpython-313-pytest-9.0.2.pyc +0 -0
- tests/__pycache__/test_rest_api.cpython-313-pytest-9.0.2.pyc +0 -0
- tests/test_constraints.py +140 -0
- 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
|