gahugo2020 commited on
Commit
e3cdb23
Β·
verified Β·
1 Parent(s): 5f93d80

Upload folder using huggingface_hub

Browse files
.github/workflows/sync.yml ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Sync to Hugging Face Hub
2
+ on:
3
+ push:
4
+ branches: [main]
5
+ workflow_dispatch:
6
+
7
+ jobs:
8
+ sync-to-hub:
9
+ runs-on: ubuntu-latest
10
+ steps:
11
+ - uses: actions/checkout@v3
12
+ with:
13
+ fetch-depth: 0
14
+ - name: Install HF Hub
15
+ run: pip install huggingface_hub
16
+ - name: Push to HF
17
+ env:
18
+ HF_TOKEN: ${{ secrets.HF_TOKEN }}
19
+ run: |
20
+ python -c "from huggingface_hub import HfApi; \
21
+ api = HfApi(); \
22
+ api.upload_folder(folder_path='.', repo_id='gahugo2020/Smart_Grad_UI', repo_type='space', token='$HF_TOKEN', \
23
+ ignore_patterns=['.git', '.gitignore', 'target/', '*.class', 'nb-configuration.xml', 'nbactions.xml'])"
Dockerfile CHANGED
@@ -1,34 +1,30 @@
1
- FROM tomcat:9.0-jdk11
 
 
 
2
 
3
- # Install git and maven
4
- RUN apt-get update && \
5
- apt-get install -y git maven && \
6
- rm -rf /var/lib/apt/lists/*
7
 
8
- # Set working directory
9
- WORKDIR /build
 
10
 
11
- # Build argument β€” passed at runtime from HF secret
12
- ARG GITHUB_PAT
13
 
14
- # Clone private Java Maven repo
15
- RUN --mount=type=secret,id=GITHUB_PAT \
16
- GITHUB_PAT=$(cat /run/secrets/GITHUB_PAT) && \
17
- git clone --depth 1 \
18
- https://${GITHUB_PAT}@github.com/pgacirane/Smart_Grad_UI.git \
19
- app
20
-
21
- # Build the WAR file
22
- RUN cd app && mvn clean package -DskipTests -q
23
 
24
- # Deploy WAR to Tomcat
25
- RUN cp app/target/*.war /usr/local/tomcat/webapps/ROOT.war
26
 
27
- # HuggingFace requires port 7860
28
- RUN sed -i 's/port="8080"/port="7860"/' /usr/local/tomcat/conf/server.xml
 
29
 
30
- # Expose port
31
  EXPOSE 7860
 
32
 
33
- # Start Tomcat
34
  CMD ["catalina.sh", "run"]
 
1
+ # Stage 1: Build
2
+ # Using JDK 11 as it has better compatibility with Java 8 source code
3
+ FROM maven:3.8.5-openjdk-11 AS build
4
+ WORKDIR /app
5
 
6
+ # Copy only pom.xml first to cache dependencies
7
+ COPY pom.xml .
8
+ RUN mvn dependency:go-offline -B
 
9
 
10
+ # Copy source and build
11
+ COPY src ./src
12
+ RUN mvn clean package -DskipTests
13
 
14
+ # Stage 2: Run (Tomcat 9 is correct for Java 8/11 and javax.servlet)
15
+ FROM tomcat:9.0-jdk11-openjdk
16
 
17
+ # Standard port adjustment for Hugging Face/Cloud environments
18
+ RUN sed -i 's/port="8080"/port="7860"/' /usr/local/tomcat/conf/server.xml
 
 
 
 
 
 
 
19
 
20
+ # Wipe default apps to prevent conflicts
21
+ RUN rm -rf /usr/local/tomcat/webapps/ROOT /usr/local/tomcat/webapps/ROOT.war
22
 
23
+ # Copy the built WAR from the build stage
24
+ # Your pom.xml defines <finalName>MyWebApp</finalName>, so it will be MyWebApp.war
25
+ COPY --from=build /app/target/MyWebApp.war /usr/local/tomcat/webapps/ROOT.war
26
 
 
27
  EXPOSE 7860
28
+ ENV PORT 7860
29
 
 
30
  CMD ["catalina.sh", "run"]
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.
pom.xml ADDED
@@ -0,0 +1,212 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project xmlns="http://maven.apache.org/POM/4.0.0"
3
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
5
+ http://maven.apache.org/xsd/maven-4.0.0.xsd">
6
+ <modelVersion>4.0.0</modelVersion>
7
+
8
+ <groupId>com.myapp</groupId>
9
+ <artifactId>CRM</artifactId>
10
+ <version>1.0</version>
11
+ <packaging>war</packaging>
12
+
13
+ <properties>
14
+ <maven.compiler.source>1.8</maven.compiler.source>
15
+ <maven.compiler.target>1.8</maven.compiler.target>
16
+ <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
17
+ <failOnMissingWebXml>false</failOnMissingWebXml>
18
+ </properties>
19
+
20
+ <dependencies>
21
+
22
+
23
+
24
+ <!-- Bcrypt for password hashing -->
25
+ <dependency>
26
+ <groupId>org.mindrot</groupId>
27
+ <artifactId>jbcrypt</artifactId>
28
+ <version>0.4</version>
29
+ </dependency>
30
+
31
+ <!-- PostgreSQL JDBC Driver -->
32
+ <dependency>
33
+ <groupId>org.postgresql</groupId>
34
+ <artifactId>postgresql</artifactId>
35
+ <version>42.7.2</version>
36
+ <scope>runtime</scope>
37
+ </dependency>
38
+
39
+
40
+ <dependency>
41
+ <groupId>io.github.cdimascio</groupId>
42
+ <artifactId>java-dotenv</artifactId>
43
+ <version>5.2.2</version>
44
+ </dependency>
45
+
46
+ <!-- JavaMail API -->
47
+ <dependency>
48
+ <groupId>com.sun.mail</groupId>
49
+ <artifactId>jakarta.mail</artifactId>
50
+ <version>2.0.1</version>
51
+ </dependency>
52
+
53
+ <!-- JSTL (Jakarta version) -->
54
+ <!-- <dependency>
55
+ <groupId>jakarta.servlet.jsp.jstl</groupId>
56
+ <artifactId>jakarta.servlet.jsp.jstl-api</artifactId>
57
+ <version>3.0.0</version>
58
+ </dependency>
59
+
60
+ -->
61
+
62
+ <dependency>
63
+ <groupId>javax.servlet</groupId>
64
+ <artifactId>jstl</artifactId>
65
+ <version>1.2</version>
66
+ </dependency>
67
+
68
+ <dependency>
69
+ <groupId>org.apache.commons</groupId>
70
+ <artifactId>commons-lang3</artifactId>
71
+ <version>3.12.0</version>
72
+ </dependency>
73
+
74
+ <dependency>
75
+ <groupId>com.google.code.gson</groupId>
76
+ <artifactId>gson</artifactId>
77
+ <version>2.10.1</version>
78
+ </dependency>
79
+
80
+
81
+
82
+
83
+
84
+ <!-- WebApp Runner for Heroku -->
85
+ <dependency>
86
+ <groupId>com.heroku</groupId>
87
+ <artifactId>webapp-runner</artifactId>
88
+ <version>9.0.41.0</version>
89
+ <scope>provided</scope>
90
+ </dependency>
91
+
92
+ <dependency>
93
+ <groupId>javax.servlet</groupId>
94
+ <artifactId>javax.servlet-api</artifactId>
95
+ <version>4.0.1</version> <!-- matches Tomcat 9 -->
96
+ <scope>provided</scope>
97
+ </dependency>
98
+
99
+
100
+
101
+ <dependency>
102
+ <groupId>javax</groupId>
103
+ <artifactId>javaee-web-api</artifactId>
104
+ <version>7.0</version>
105
+ <scope>provided</scope>
106
+ </dependency>
107
+
108
+ <dependency>
109
+ <groupId>org.json</groupId>
110
+ <artifactId>json</artifactId>
111
+ <version>20231013</version>
112
+ </dependency>
113
+
114
+
115
+ <!-- Jackson Core -->
116
+ <dependency>
117
+ <groupId>com.fasterxml.jackson.core</groupId>
118
+ <artifactId>jackson-core</artifactId>
119
+ <version>2.13.5</version>
120
+ </dependency>
121
+
122
+ <!-- Jackson Databind (contains ObjectMapper) -->
123
+ <dependency>
124
+ <groupId>com.fasterxml.jackson.core</groupId>
125
+ <artifactId>jackson-databind</artifactId>
126
+ <version>2.13.5</version>
127
+ </dependency>
128
+
129
+ <!-- Jackson Annotations -->
130
+ <dependency>
131
+ <groupId>com.fasterxml.jackson.core</groupId>
132
+ <artifactId>jackson-annotations</artifactId>
133
+ <version>2.13.5</version>
134
+ </dependency>
135
+
136
+
137
+
138
+
139
+ </dependencies>
140
+
141
+ <build>
142
+ <finalName>MyWebApp</finalName>
143
+ <plugins>
144
+ <!-- WAR packaging plugin -->
145
+ <plugin>
146
+ <artifactId>maven-war-plugin</artifactId>
147
+ <version>3.3.1</version>
148
+ <configuration>
149
+ <failOnMissingWebXml>false</failOnMissingWebXml>
150
+ </configuration>
151
+ </plugin>
152
+
153
+ <!-- Heroku Maven Plugin -->
154
+ <plugin>
155
+ <groupId>com.heroku.sdk</groupId>
156
+ <artifactId>heroku-maven-plugin</artifactId>
157
+ <version>3.0.4</version>
158
+ <configuration>
159
+ <appName>your-heroku-app-name</appName>
160
+ </configuration>
161
+ </plugin>
162
+
163
+ <!-- Maven Compiler Plugin -->
164
+ <plugin>
165
+ <artifactId>maven-compiler-plugin</artifactId>
166
+ <version>3.8.1</version>
167
+ <configuration>
168
+ <source>1.8</source>
169
+ <target>1.8</target>
170
+ </configuration>
171
+ </plugin>
172
+
173
+ <!-- Dependency plugin for copying webapp-runner -->
174
+ <plugin>
175
+ <groupId>org.apache.maven.plugins</groupId>
176
+ <artifactId>maven-dependency-plugin</artifactId>
177
+ <version>3.1.1</version>
178
+ <executions>
179
+ <execution>
180
+ <phase>package</phase>
181
+ <goals><goal>copy</goal></goals>
182
+ <configuration>
183
+ <artifactItems>
184
+ <artifactItem>
185
+ <groupId>com.heroku</groupId>
186
+ <artifactId>webapp-runner</artifactId>
187
+ <version>9.0.41.0</version>
188
+ <destFileName>webapp-runner.jar</destFileName>
189
+ </artifactItem>
190
+ </artifactItems>
191
+ </configuration>
192
+ </execution>
193
+ </executions>
194
+ </plugin>
195
+ </plugins>
196
+ </build>
197
+
198
+ <repositories>
199
+ <repository>
200
+ <id>central</id>
201
+ <url>https://repo.maven.apache.org/maven2</url>
202
+ </repository>
203
+ </repositories>
204
+
205
+ <pluginRepositories>
206
+ <pluginRepository>
207
+ <id>central</id>
208
+ <url>https://repo.maven.apache.org/maven2</url>
209
+ </pluginRepository>
210
+ </pluginRepositories>
211
+ <name>Smart_Grad_UI</name>
212
+ </project>
src/main/java/CrewAIUtil/Communicator.java ADDED
@@ -0,0 +1,253 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package CrewAIUtil;
2
+
3
+ import io.github.cdimascio.dotenv.Dotenv;
4
+ import java.io.*;
5
+ import java.net.HttpURLConnection;
6
+ import java.net.SocketTimeoutException;
7
+ import java.net.URL;
8
+ import org.json.JSONObject;
9
+ import org.json.JSONTokener;
10
+
11
+ /**
12
+ * Robust Communicator for CrewAI backend (HuggingFace Space)
13
+ */
14
+ public class Communicator {
15
+
16
+
17
+ private static final Dotenv dotenv;
18
+ static {
19
+ Dotenv tempDotenv = null;
20
+ try {
21
+ tempDotenv = Dotenv.configure()
22
+ .filename("/.env") // Leading slash instructs classpath lookup
23
+ .ignoreIfMalformed()
24
+ .ignoreIfMissing()
25
+ .load();
26
+ } catch (Exception e) {
27
+ // Suppress initialization container logging errors safely
28
+ }
29
+ dotenv = tempDotenv;
30
+ }
31
+
32
+ private static String getEnvVar(String key) {
33
+ if (dotenv != null) {
34
+ String value = dotenv.get(key);
35
+ if (value != null && !value.trim().isEmpty()) {
36
+ return value.trim();
37
+ }
38
+ }
39
+ // Fallback directly to System OS Environment or Properties
40
+ String systemVal = System.getenv(key);
41
+ if (systemVal != null && !systemVal.trim().isEmpty()) {
42
+ return systemVal.trim();
43
+ }
44
+ return System.getProperty(key, "");
45
+ }
46
+
47
+ private static final String BASE_URL = getEnvVar("BASE_URL");
48
+
49
+ private static final int CONNECT_TIMEOUT = 15000; // 15 sec
50
+ private static final int READ_TIMEOUT = 300000; // 5 min
51
+ private static final int MAX_RETRIES = 3;
52
+
53
+ // ─── Wake up HF Space ─────────────────────────────────────────────
54
+ public static void wakeUpSpace() {
55
+ try {
56
+ URL url = new URL(BASE_URL + "/");
57
+ HttpURLConnection conn = (HttpURLConnection) url.openConnection();
58
+ conn.setRequestMethod("GET");
59
+ conn.setConnectTimeout(CONNECT_TIMEOUT);
60
+ conn.setReadTimeout(CONNECT_TIMEOUT);
61
+
62
+ int responseCode = conn.getResponseCode();
63
+ System.out.println("Wake-up ping response: " + responseCode);
64
+
65
+ conn.disconnect();
66
+ } catch (Exception e) {
67
+ System.out.println("Wake-up failed: " + e.getMessage());
68
+ }
69
+ }
70
+
71
+ // ─── Blocking call ────────────────────────────────────────────────
72
+ public static String returnOutputFromCrew(String userQuery) {
73
+ try {
74
+ JSONObject payload = new JSONObject();
75
+ //payload.put("customer_id", customerId);
76
+ payload.put("user_query", userQuery);
77
+
78
+ System.out.println("Sending request to /run ...");
79
+
80
+ JSONObject responseJson = postRequestWithRetry(BASE_URL + "/kickoff", payload);
81
+
82
+ if (responseJson == null) {
83
+ return "Error: No response from server after retries.";
84
+ }
85
+
86
+ String result = responseJson.optString("result", null);
87
+ if (result != null) return result;
88
+
89
+ return "Unexpected response: " + responseJson.toString(2);
90
+
91
+ } catch (Exception e) {
92
+ e.printStackTrace();
93
+ return "Exception: " + e.getMessage();
94
+ }
95
+ }
96
+
97
+ // ─── Streaming-style output (still backend must support SSE for true streaming) ───
98
+ public static void returnOutputFromCrew(String userQuery, PrintWriter writer) {
99
+ try {
100
+ JSONObject payload = new JSONObject();
101
+ //payload.put("customer_id", customerId);
102
+ payload.put("user_query", userQuery);
103
+
104
+ writer.println("data: ⏳ Processing request...\n");
105
+ writer.flush();
106
+
107
+ JSONObject responseJson = postRequestWithRetry(BASE_URL + "/run-mega", payload);
108
+
109
+ if (responseJson == null) {
110
+ writer.println("data: ❌ No response after retries.\n\n");
111
+ writer.println("data: [END]\n\n");
112
+ writer.flush();
113
+ return;
114
+ }
115
+
116
+ String result = responseJson.optString("result", null);
117
+
118
+ if (result != null) {
119
+ for (String line : result.split("\n")) {
120
+ writer.println("data: " + line);
121
+ writer.flush();
122
+ }
123
+ } else {
124
+ writer.println("data: ⚠️ Unexpected response: " + responseJson.toString().replace("\n", " "));
125
+ }
126
+
127
+ writer.println("data: [END]\n\n");
128
+ writer.flush();
129
+
130
+ } catch (Exception e) {
131
+ writer.println("data: ❌ Exception: " + e.getMessage() + "\n\n");
132
+ writer.println("data: [END]\n\n");
133
+ writer.flush();
134
+ }
135
+ }
136
+
137
+ // ─── POST with retry logic ──────────────────��─────────────────────
138
+ public static JSONObject postRequestWithRetry(String urlString, JSONObject jsonData) {
139
+
140
+ for (int attempt = 1; attempt <= MAX_RETRIES; attempt++) {
141
+ try {
142
+ System.out.println("Attempt " + attempt + "...");
143
+
144
+ JSONObject response = postRequest(urlString, jsonData);
145
+
146
+ if (response != null) {
147
+ return response;
148
+ }
149
+
150
+ } catch (Exception e) {
151
+ System.out.println("Attempt " + attempt + " failed: " + e.getMessage());
152
+ }
153
+
154
+ // wait before retry
155
+ try {
156
+ Thread.sleep(3000);
157
+ } catch (InterruptedException ignored) {}
158
+ }
159
+
160
+ return null;
161
+ }
162
+
163
+ // ─── POST helper ──────────────────────────────────────────────────
164
+ public static JSONObject postRequest(String urlString, JSONObject jsonData) {
165
+
166
+ HttpURLConnection conn = null;
167
+
168
+ try {
169
+ URL url = new URL(urlString);
170
+ conn = (HttpURLConnection) url.openConnection();
171
+
172
+ conn.setConnectTimeout(CONNECT_TIMEOUT);
173
+ conn.setReadTimeout(READ_TIMEOUT);
174
+ conn.setRequestMethod("POST");
175
+ conn.setRequestProperty("Content-Type", "application/json");
176
+ conn.setDoOutput(true);
177
+
178
+ // Send request
179
+ try (OutputStream os = conn.getOutputStream()) {
180
+ os.write(jsonData.toString().getBytes("UTF-8"));
181
+ os.flush();
182
+ }
183
+
184
+ long startTime = System.currentTimeMillis();
185
+
186
+ int status = conn.getResponseCode();
187
+
188
+ long duration = System.currentTimeMillis() - startTime;
189
+ System.out.println("Response time: " + duration + " ms");
190
+
191
+ // Accept 200 OK and 202 Accepted (/kickoff returns 202)
192
+ if (status != 200 && status != 202) {
193
+ System.out.println("POST failed: " + status + " " + conn.getResponseMessage());
194
+ return null;
195
+ }
196
+
197
+ try (InputStream is = conn.getInputStream()) {
198
+ return new JSONObject(new JSONTokener(is));
199
+ }
200
+
201
+ } catch (SocketTimeoutException e) {
202
+ System.out.println("Timeout: server took too long.");
203
+ return null;
204
+
205
+ } catch (IOException e) {
206
+ System.out.println("POST request failed: " + e.getMessage());
207
+ return null;
208
+
209
+ } finally {
210
+ if (conn != null) conn.disconnect();
211
+ }
212
+ }
213
+
214
+ // ─── GET helper ───────────────────────────────────────────────────
215
+ public static JSONObject getRequest(String urlString) {
216
+
217
+ HttpURLConnection conn = null;
218
+
219
+ try {
220
+ URL url = new URL(urlString);
221
+ conn = (HttpURLConnection) url.openConnection();
222
+
223
+ conn.setConnectTimeout(CONNECT_TIMEOUT);
224
+ conn.setReadTimeout(60000);
225
+ conn.setRequestMethod("GET");
226
+
227
+ int status = conn.getResponseCode();
228
+
229
+ if (status != 200) {
230
+ System.out.println("GET failed: " + status);
231
+ return null;
232
+ }
233
+
234
+ try (InputStream is = conn.getInputStream()) {
235
+ return new JSONObject(new JSONTokener(is));
236
+ }
237
+
238
+ } catch (IOException e) {
239
+ System.out.println("GET request failed: " + e.getMessage());
240
+ return null;
241
+
242
+ } finally {
243
+ if (conn != null) conn.disconnect();
244
+ }
245
+ }
246
+ //ok
247
+ // ─── Test ─────────────────────────────────────────────────────────
248
+ public static void main(String[] args) {
249
+ wakeUpSpace();
250
+ String result = returnOutputFromCrew("What is ART?");
251
+ System.out.println("Final result:\n" + result);
252
+ }
253
+ }
src/main/java/CrewAIUtil/CrewResponseStream.java ADDED
@@ -0,0 +1,238 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package CrewAIUtil;
2
+
3
+ import io.github.cdimascio.dotenv.Dotenv;
4
+ import org.json.JSONObject;
5
+ import javax.servlet.ServletException;
6
+ import javax.servlet.annotation.WebServlet;
7
+ import javax.servlet.http.*;
8
+ import java.io.IOException;
9
+ import java.io.PrintWriter;
10
+
11
+ /**
12
+ * SSE Servlet for SmartGrad AI Advisory System.
13
+ * Handles the async kickoff β†’ polling pattern:
14
+ * 1. POST to /kickoff β†’ receive job_id
15
+ * 2. Poll /status/{job_id} until complete
16
+ * 3. Stream progress + final result via SSE
17
+ *
18
+ * Called by think.jsp:
19
+ * /CrewResponseStream?student_name=...&program=...&country=...&...
20
+ *
21
+ * @author patrick.gacirane
22
+ */
23
+ @WebServlet(name = "CrewResponseStreamServlet", urlPatterns = {"/CrewResponseStream"})
24
+ public class CrewResponseStream extends HttpServlet {
25
+
26
+
27
+ private static final Dotenv dotenv;
28
+ static {
29
+ Dotenv tempDotenv = null;
30
+ try {
31
+ tempDotenv = Dotenv.configure()
32
+ .filename("/.env") // Leading slash instructs classpath lookup
33
+ .ignoreIfMalformed()
34
+ .ignoreIfMissing()
35
+ .load();
36
+ } catch (Exception e) {
37
+ // Suppress initialization container logging errors safely
38
+ }
39
+ dotenv = tempDotenv;
40
+ }
41
+
42
+ private static String getEnvVar(String key) {
43
+ if (dotenv != null) {
44
+ String value = dotenv.get(key);
45
+ if (value != null && !value.trim().isEmpty()) {
46
+ return value.trim();
47
+ }
48
+ }
49
+ // Fallback directly to System OS Environment or Properties
50
+ String systemVal = System.getenv(key);
51
+ if (systemVal != null && !systemVal.trim().isEmpty()) {
52
+ return systemVal.trim();
53
+ }
54
+ return System.getProperty(key, "");
55
+ }
56
+
57
+ private static final String BASE_URL = getEnvVar("BASE_URL");
58
+
59
+
60
+
61
+
62
+ private static final int POLL_DELAY = 5000; // ms between status polls
63
+ private static final int MAX_POLLS = 72; // 72 Γ— 5s = 6 minutes max wait
64
+
65
+ @Override
66
+ protected void doGet(HttpServletRequest request, HttpServletResponse response)
67
+ throws ServletException, IOException {
68
+
69
+ // ── Cache control ─────────────────────────────────────────────────────
70
+ response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
71
+ response.setHeader("Pragma", "no-cache");
72
+ response.setDateHeader("Expires", 0);
73
+
74
+ // ── SSE headers ───────────────────────────────────────────────────────
75
+ response.setContentType("text/event-stream");
76
+ response.setCharacterEncoding("UTF-8");
77
+
78
+ PrintWriter out = response.getWriter();
79
+
80
+ // ── Read all student fields ───────────────────────────────────────────
81
+ String studentName = param(request, "student_name");
82
+ String lowerLevel = param(request, "lower_level", "high school");
83
+ String higherLevel = param(request, "higher_level", "undergraduate");
84
+ String grade = param(request, "grade");
85
+ String major = param(request, "major");
86
+ String subjects = param(request, "subjects");
87
+ String program = param(request, "program");
88
+ String country = param(request, "country");
89
+ String destinationCountry = param(request, "destination_country");
90
+ String state = param(request, "state");
91
+ String agentName = param(request, "agent_name", "SmartGrad AI Advisor");
92
+ String institution = param(request, "institution", "SmartGrad Education Services");
93
+
94
+ // ── Validate required fields ──────────────────────────────────────────
95
+ if (studentName.isEmpty() || program.isEmpty() || country.isEmpty() || destinationCountry.isEmpty()) {
96
+ sendMessage(out, "❌ Error: Required fields missing (student_name, program, country, destination_country).");
97
+ endStream(out);
98
+ return;
99
+ }
100
+
101
+ try {
102
+ // ── Step 0: Wake up the HF Space ─────────────────────────────────
103
+ sendStatus(out, "Connecting to SmartGrad advisory system...");
104
+ Communicator.wakeUpSpace();
105
+
106
+ // ── Step 1: Build payload and POST to /kickoff ────────────────────
107
+ JSONObject payload = new JSONObject();
108
+ payload.put("student_name", studentName);
109
+ payload.put("lower_level", lowerLevel);
110
+ payload.put("higher_level", higherLevel);
111
+ payload.put("grade", grade);
112
+ payload.put("major", major);
113
+ payload.put("subjects", subjects);
114
+ payload.put("program", program);
115
+ payload.put("country", country);
116
+ payload.put("destination_country", destinationCountry);
117
+ payload.put("state", state);
118
+ payload.put("agent_name", agentName);
119
+ payload.put("institution", institution);
120
+
121
+ sendStatus(out, "Submitting student profile to agents...");
122
+
123
+ JSONObject kickoffResp = Communicator.postRequest(BASE_URL + "/kickoff", payload);
124
+
125
+ if (kickoffResp == null) {
126
+ sendMessage(out, "❌ Error: Could not reach the advisory system. Please try again.");
127
+ endStream(out);
128
+ return;
129
+ }
130
+
131
+ String jobId = kickoffResp.optString("job_id", null);
132
+ if (jobId == null || jobId.isEmpty()) {
133
+ sendMessage(out, "❌ Error: No job ID returned from server.");
134
+ endStream(out);
135
+ return;
136
+ }
137
+
138
+ sendStatus(out, "Crew started (Job ID: " + jobId.substring(0, 8) + "...). Agents are researching...");
139
+
140
+ // ── Step 2: Poll /status/{job_id} until done ──────────────────────
141
+ String statusUrl = BASE_URL + "/status/" + jobId;
142
+ int polls = 0;
143
+ int stepsLogged = 0;
144
+
145
+ while (polls < MAX_POLLS) {
146
+ Thread.sleep(POLL_DELAY);
147
+ polls++;
148
+
149
+ JSONObject statusResp = Communicator.getRequest(statusUrl);
150
+
151
+ if (statusResp == null) {
152
+ sendStatus(out, "Still waiting for agents... (attempt " + polls + ")");
153
+ continue;
154
+ }
155
+
156
+ String status = statusResp.optString("status", "unknown");
157
+ int newSteps = statusResp.optInt("steps_logged", 0);
158
+
159
+ // Show progress when new steps arrive
160
+ if (newSteps > stepsLogged) {
161
+ int diff = newSteps - stepsLogged;
162
+ stepsLogged = newSteps;
163
+ sendStatus(out, "Agents completed " + newSteps + " step" + (newSteps == 1 ? "" : "s") + "...");
164
+ }
165
+
166
+ if ("complete".equals(status)) {
167
+ String result = statusResp.optString("result", null);
168
+
169
+ if (result != null && !result.isEmpty()) {
170
+ // Stream the result line by line
171
+ for (String line : result.split("\n")) {
172
+ sendMessage(out, line);
173
+ }
174
+ } else {
175
+ sendMessage(out, "⚠️ Agents completed but returned an empty result.");
176
+ }
177
+ endStream(out);
178
+ return;
179
+
180
+ } else if ("error".equals(status)) {
181
+ String error = statusResp.optString("error", "Unknown error from advisory system.");
182
+ sendMessage(out, "❌ Advisory system error: " + error);
183
+ endStream(out);
184
+ return;
185
+
186
+ } else if ("pending".equals(status) || "running".equals(status)) {
187
+ // Still working β€” keep polling
188
+ String label = "running".equals(status)
189
+ ? "Agents are analyzing your profile... (" + polls + "/" + MAX_POLLS + ")"
190
+ : "Waiting for crew to start... (" + polls + "/" + MAX_POLLS + ")";
191
+ sendStatus(out, label);
192
+ }
193
+ }
194
+
195
+ // Timeout
196
+ sendMessage(out, "⚠️ The advisory system is taking longer than expected. Please check back using job ID: " + jobId);
197
+ endStream(out);
198
+
199
+ } catch (InterruptedException ie) {
200
+ Thread.currentThread().interrupt();
201
+ sendMessage(out, "❌ Request interrupted.");
202
+ endStream(out);
203
+ } catch (Exception e) {
204
+ e.printStackTrace();
205
+ sendMessage(out, "❌ Exception: " + e.getMessage());
206
+ endStream(out);
207
+ }
208
+ }
209
+
210
+ // ── Helpers ───────────────────────────────────────────────────────────────
211
+
212
+ private String param(HttpServletRequest req, String name) {
213
+ String v = req.getParameter(name);
214
+ return (v != null) ? v.trim() : "";
215
+ }
216
+
217
+ private String param(HttpServletRequest req, String name, String defaultVal) {
218
+ String v = req.getParameter(name);
219
+ return (v != null && !v.trim().isEmpty()) ? v.trim() : defaultVal;
220
+ }
221
+
222
+ /** Sends a result/content line to the browser. */
223
+ private void sendMessage(PrintWriter out, String message) {
224
+ out.write("data: " + message.replace("\n", "\ndata: ") + "\n\n");
225
+ out.flush();
226
+ }
227
+
228
+ /** Sends a STATUS: prefix line β€” picked up by JS as a progress update. */
229
+ private void sendStatus(PrintWriter out, String statusMsg) {
230
+ out.write("data: STATUS:" + statusMsg + "\n\n");
231
+ out.flush();
232
+ }
233
+
234
+ private void endStream(PrintWriter out) {
235
+ out.write("data: [END]\n\n");
236
+ out.flush();
237
+ }
238
+ }
src/main/java/servlet/ChatServlet.java ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package servlet;
2
+
3
+
4
+
5
+ import javax.servlet.*;
6
+ import javax.servlet.http.*;
7
+ import java.io.IOException;
8
+ import java.sql.SQLException;
9
+ import java.util.logging.Level;
10
+ import java.util.logging.Logger;
11
+ import javax.servlet.annotation.WebServlet;
12
+
13
+ @WebServlet(name = "/ChatServlet", urlPatterns = {"/chat"})
14
+ public class ChatServlet extends HttpServlet {
15
+
16
+ /**
17
+ * Handles the HTTP GET request.
18
+ * Displays the login page.
19
+ */
20
+ @Override
21
+ protected void doGet(HttpServletRequest request, HttpServletResponse response)
22
+ throws ServletException, IOException {
23
+ // Forward to the login JSP located in the protected WEB-INF folder
24
+ request.getRequestDispatcher("/WEB-INF/query_pages/think.jsp").forward(request, response);
25
+ }
26
+
27
+ /**
28
+ * Handles the HTTP POST request.
29
+ * Validates credentials and redirects based on user role.
30
+ */
31
+ @Override
32
+ protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
33
+ //String email = request.getParameter("email");
34
+ //String password = request.getParameter("password");
35
+
36
+
37
+
38
+ try {
39
+
40
+
41
+
42
+ response.sendRedirect(request.getContextPath() + "/CrewResponseStream");
43
+
44
+
45
+ } catch (Exception e) {
46
+ e.printStackTrace();
47
+ request.setAttribute("error", "Database error: " + e.getMessage());
48
+ request.getRequestDispatcher("/WEB-INF/query_pages/think.jsp").forward(request, response);
49
+ }
50
+ }
51
+ }
src/main/resources/META-INF/persistence.xml ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <persistence version="2.2" xmlns="http://xmlns.jcp.org/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_2.xsd">
3
+ <!-- Define Persistence Unit -->
4
+ <persistence-unit name="my_persistence_unit">
5
+
6
+ </persistence-unit>
7
+ </persistence>
src/main/webapp/META-INF/context.xml ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <Context path="/Smart_Grad_UI"/>
src/main/webapp/WEB-INF/beans.xml ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <beans xmlns="http://xmlns.jcp.org/xml/ns/javaee"
3
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4
+ xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/beans_2_0.xsd"
5
+ bean-discovery-mode="all">
6
+ </beans>
src/main/webapp/WEB-INF/query_pages/think.jsp ADDED
@@ -0,0 +1,867 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <%@ page contentType="text/html;charset=UTF-8" language="java" %>
2
+ <%
3
+ response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
4
+ response.setHeader("Pragma", "no-cache");
5
+ response.setDateHeader("Expires", 0);
6
+ String ctxPath = request.getContextPath();
7
+ %>
8
+ <!DOCTYPE html>
9
+ <html lang="en">
10
+ <head>
11
+ <meta charset="UTF-8"/>
12
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"/>
13
+ <title>SmartGrad AI Advisor</title>
14
+ <link rel="preconnect" href="https://fonts.googleapis.com"/>
15
+ <link href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,wght@0,400;0,500;0,600&family=Syne:wght@600;700&display=swap" rel="stylesheet"/>
16
+ <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
17
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>
18
+ <style>
19
+ /* ─── TOKENS ──────────────────────────────────────────── */
20
+ :root {
21
+ --bg: #0f0f13;
22
+ --surf: #16161d;
23
+ --surf2: #1c1c26;
24
+ --bdr: #24242f;
25
+ --bdr2: #2e2e3d;
26
+ --txt: #e8e8f0;
27
+ --txt2: #7a7a95;
28
+ --txt3: #44445a;
29
+ --acc: #6c47ff;
30
+ --acc2: #9b7aff;
31
+ --acc-hi: #b39aff;
32
+ --acc-glow: rgba(108,71,255,.18);
33
+ --green: #22d87a;
34
+ --red: #ff6b6b;
35
+ --yellow: #ffc85c;
36
+ --hh: 56px; /* header height */
37
+ --fw: 318px; /* form panel width desktop */
38
+ --fn: 'DM Sans', sans-serif;
39
+ --fd: 'Syne', sans-serif;
40
+ --drawer-ease: cubic-bezier(.32,1,.28,1);
41
+ }
42
+
43
+ /* ─── RESET ───────────────────────────────────────────── */
44
+ *,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
45
+ html{height:100%;-webkit-text-size-adjust:100%}
46
+ body{
47
+ height:100%;
48
+ background:var(--bg);
49
+ color:var(--txt);
50
+ font-family:var(--fn);
51
+ font-size:14px;
52
+ line-height:1.65;
53
+ overflow:hidden;
54
+ }
55
+
56
+ /* ─── APP SHELL ───────────────────────────────────────── */
57
+ #app{position:fixed;inset:0;display:flex;flex-direction:column}
58
+
59
+ /* ─── HEADER ──────────────────────────────────────────── */
60
+ #hdr{
61
+ flex-shrink:0;
62
+ height:var(--hh);
63
+ background:#0c0c11;
64
+ border-bottom:1px solid var(--bdr2);
65
+ display:flex;align-items:center;justify-content:space-between;
66
+ padding:0 clamp(12px,3vw,22px);
67
+ gap:10px;
68
+ z-index:400;
69
+ position:relative;
70
+ }
71
+ .hl{display:flex;align-items:center;gap:10px;min-width:0}
72
+ .hi{
73
+ width:34px;height:34px;flex-shrink:0;
74
+ background:linear-gradient(135deg,var(--acc),var(--acc2));
75
+ border-radius:9px;
76
+ display:flex;align-items:center;justify-content:center;
77
+ font-size:17px;
78
+ box-shadow:0 0 14px var(--acc-glow);
79
+ }
80
+ .ht{font-family:var(--fd);font-size:clamp(13px,2.5vw,15px);font-weight:700;color:#fff;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
81
+ .hs{font-size:11px;color:var(--txt2);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
82
+ .hr{display:flex;align-items:center;gap:8px;flex-shrink:0}
83
+
84
+ /* quota pill */
85
+ #qpill{
86
+ border:1px solid var(--bdr2);background:#16161d;
87
+ border-radius:20px;padding:4px 10px;
88
+ font-size:11px;color:var(--txt2);white-space:nowrap;
89
+ }
90
+ #qpill.warn{border-color:var(--yellow);color:var(--yellow)}
91
+ #qpill.full{border-color:var(--red);color:var(--red)}
92
+
93
+ .spill{
94
+ display:flex;align-items:center;gap:6px;
95
+ background:#16161d;border:1px solid var(--bdr2);
96
+ border-radius:20px;padding:4px 12px;
97
+ font-size:clamp(10px,1.8vw,12px);color:var(--txt2);white-space:nowrap;
98
+ }
99
+ .sdot{
100
+ width:7px;height:7px;border-radius:50%;
101
+ background:var(--green);box-shadow:0 0 6px var(--green);
102
+ animation:blink 2.2s infinite;
103
+ }
104
+ @keyframes blink{0%,100%{opacity:1}50%{opacity:.2}}
105
+
106
+ /* ─── MOBILE FORM TOGGLE BAR ──────────────────────────── */
107
+ /* Shown only on mobile; sits just below the main header */
108
+ #mob-toggle{
109
+ display:none; /* shown via media query */
110
+ flex-shrink:0;
111
+ align-items:center;justify-content:space-between;
112
+ background:var(--surf);
113
+ border-bottom:1px solid var(--bdr2);
114
+ padding:0 16px;
115
+ height:44px;
116
+ cursor:pointer;
117
+ user-select:none;
118
+ -webkit-tap-highlight-color:transparent;
119
+ transition:background .15s;
120
+ z-index:10;
121
+ }
122
+ #mob-toggle:active{background:var(--surf2)}
123
+ .mt-left{display:flex;align-items:center;gap:8px}
124
+ .mt-icon{
125
+ width:28px;height:28px;
126
+ background:linear-gradient(135deg,var(--acc),var(--acc2));
127
+ border-radius:7px;
128
+ display:flex;align-items:center;justify-content:center;
129
+ font-size:14px;flex-shrink:0;
130
+ }
131
+ .mt-label{font-family:var(--fd);font-size:12px;font-weight:700;color:var(--acc-hi);letter-spacing:.5px;text-transform:uppercase}
132
+ .mt-hint{font-size:11px;color:var(--txt3)}
133
+ .mt-arrow{
134
+ font-size:11px;color:var(--txt2);
135
+ transition:transform .3s var(--drawer-ease);
136
+ flex-shrink:0;
137
+ }
138
+ .mt-arrow.open{transform:rotate(180deg)}
139
+
140
+ /* ─── PROGRESS BAR ────────────────────────────────────── */
141
+ #pbar{display:none;flex-shrink:0;height:3px;background:var(--bdr)}
142
+ #pfill{
143
+ height:100%;width:0%;
144
+ background:linear-gradient(90deg,var(--acc),var(--acc2));
145
+ transition:width .5s ease;
146
+ }
147
+
148
+ /* ─── MAIN ROW ────────────────────────────────────────── */
149
+ #main{flex:1;display:flex;overflow:hidden;position:relative}
150
+
151
+ /* ─── DESKTOP FORM PANEL ──────────────────────────────── */
152
+ #form-panel{
153
+ width:var(--fw);
154
+ flex-shrink:0;
155
+ background:var(--surf);
156
+ border-right:1px solid var(--bdr);
157
+ overflow-y:auto;
158
+ padding:18px 14px 22px;
159
+ -webkit-overflow-scrolling:touch;
160
+ }
161
+ #form-panel::-webkit-scrollbar{width:4px}
162
+ #form-panel::-webkit-scrollbar-thumb{background:var(--bdr2);border-radius:2px}
163
+
164
+ /* ─── SHARED FORM STYLES (used in both panel & drawer) ── */
165
+ .form-ttl{
166
+ font-family:var(--fd);font-size:11px;font-weight:700;
167
+ color:var(--acc-hi);letter-spacing:.8px;text-transform:uppercase;
168
+ margin-bottom:14px;
169
+ }
170
+ .fld{margin-bottom:10px}
171
+ .fld label{display:block;font-size:11px;color:var(--txt2);margin-bottom:4px;font-weight:500}
172
+ .fld input,.fld select,.fld textarea{
173
+ width:100%;
174
+ background:var(--surf2);
175
+ border:1px solid var(--bdr2);
176
+ border-radius:8px;color:var(--txt);
177
+ font-family:var(--fn);font-size:13px;
178
+ padding:8px 10px;outline:none;
179
+ transition:border-color .15s,box-shadow .15s;
180
+ -webkit-appearance:none;appearance:none;
181
+ }
182
+ .fld input:focus,.fld select:focus,.fld textarea:focus{
183
+ border-color:var(--acc);
184
+ box-shadow:0 0 0 3px var(--acc-glow);
185
+ }
186
+ .fld select{
187
+ background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='11' height='7' viewBox='0 0 11 7'%3E%3Cpath d='M1 1l4.5 4.5L10 1' stroke='%237a7a95' stroke-width='1.5' fill='none' stroke-linecap='round'/%3E%3C/svg%3E");
188
+ background-repeat:no-repeat;background-position:right 10px center;padding-right:28px;
189
+ }
190
+ .fld select option{background:var(--surf2)}
191
+ .fld textarea{resize:vertical;min-height:52px}
192
+ .fld input.invalid{border-color:var(--red)!important;box-shadow:0 0 0 3px rgba(255,107,107,.15)!important}
193
+
194
+ .sub-btn{
195
+ width:100%;margin-top:8px;padding:11px;
196
+ background:linear-gradient(135deg,var(--acc),var(--acc2));
197
+ border:none;border-radius:9px;color:#fff;
198
+ font-family:var(--fd);font-size:13px;font-weight:700;
199
+ cursor:pointer;
200
+ transition:opacity .15s,transform .1s;
201
+ touch-action:manipulation;
202
+ box-shadow:0 4px 18px var(--acc-glow);
203
+ }
204
+ .sub-btn:hover:not(:disabled){opacity:.88}
205
+ .sub-btn:active:not(:disabled){transform:scale(.97)}
206
+ .sub-btn:disabled{opacity:.45;cursor:not-allowed;box-shadow:none}
207
+
208
+ .qwarn{
209
+ display:none;margin-top:10px;
210
+ padding:10px 12px;
211
+ border-radius:8px;font-size:12px;line-height:1.5;text-align:center;
212
+ }
213
+ .qwarn.lim{background:rgba(255,107,107,.08);border:1px solid rgba(255,107,107,.3);color:#ff9999}
214
+ .qwarn.one{background:rgba(255,200,92,.06);border:1px solid rgba(255,200,92,.3);color:var(--yellow)}
215
+
216
+ /* ─── MOBILE DRAWER ───────────────────────────────────── */
217
+ /* Slides down from just below the toggle bar */
218
+ #mob-drawer{
219
+ display:none; /* shown via media query */
220
+ position:fixed;
221
+ /* top is set by JS after layout */
222
+ left:0;right:0;
223
+ z-index:300;
224
+ background:var(--surf);
225
+ border-bottom:1px solid var(--bdr2);
226
+ overflow:hidden;
227
+ /* height transitions for smooth open/close */
228
+ max-height:0;
229
+ transition:max-height .38s var(--drawer-ease);
230
+ box-shadow:0 8px 32px rgba(0,0,0,.55);
231
+ }
232
+ #mob-drawer.open{
233
+ max-height:80dvh; /* clamp to 80% of viewport */
234
+ overflow-y:auto;
235
+ }
236
+ #mob-drawer::-webkit-scrollbar{width:4px}
237
+ #mob-drawer::-webkit-scrollbar-thumb{background:var(--bdr2);border-radius:2px}
238
+
239
+ #mob-drawer-inner{padding:14px 16px 20px}
240
+
241
+ /* ─── CHAT PANEL ──────────────────────────────────────── */
242
+ #chat-panel{flex:1;display:flex;flex-direction:column;overflow:hidden;min-width:0}
243
+ #msgs-area{
244
+ flex:1;overflow-y:auto;overflow-x:hidden;
245
+ padding:clamp(16px,4vw,28px) 0 12px;
246
+ scroll-behavior:smooth;
247
+ -webkit-overflow-scrolling:touch;
248
+ }
249
+ #msgs-area::-webkit-scrollbar{width:5px}
250
+ #msgs-area::-webkit-scrollbar-thumb{background:#333;border-radius:3px}
251
+
252
+ .mrow{
253
+ max-width:min(780px,100%);
254
+ margin:0 auto 24px;
255
+ padding:0 clamp(12px,3vw,24px);
256
+ animation:fin .22s ease;
257
+ }
258
+ @keyframes fin{from{opacity:0;transform:translateY(5px)}to{opacity:1;transform:none}}
259
+
260
+ .urow{display:flex;justify-content:flex-end}
261
+ .ubub{
262
+ background:#1e1d2e;border:1px solid #3a3460;
263
+ border-radius:18px 18px 4px 18px;
264
+ padding:11px 16px;max-width:min(82%,520px);
265
+ color:var(--txt);font-size:13.5px;word-break:break-word;
266
+ }
267
+ .brow{display:flex;gap:12px;align-items:flex-start}
268
+ .bav{
269
+ width:30px;height:30px;border-radius:50%;flex-shrink:0;
270
+ background:linear-gradient(135deg,var(--acc),var(--acc2));
271
+ display:flex;align-items:center;justify-content:center;
272
+ font-size:15px;margin-top:2px;
273
+ box-shadow:0 0 10px var(--acc-glow);
274
+ }
275
+ .bcon{flex:1;min-width:0}
276
+ .bname{font-size:12px;font-weight:600;color:var(--txt2);margin-bottom:6px}
277
+ .btxt{color:var(--txt);font-size:clamp(13px,2.2vw,14.5px);line-height:1.75}
278
+
279
+ .btxt h1,.btxt h2,.btxt h3{color:#fff;margin:16px 0 7px;font-family:var(--fd)}
280
+ .btxt h1{font-size:17px;border-bottom:1px solid var(--bdr2);padding-bottom:5px}
281
+ .btxt h2{font-size:15px}
282
+ .btxt h3{font-size:13.5px;color:var(--acc-hi)}
283
+ .btxt p{margin-bottom:9px}
284
+ .btxt ul,.btxt ol{padding-left:20px;margin-bottom:9px}
285
+ .btxt li{margin-bottom:4px}
286
+ .btxt strong{color:#c8c0ff;font-weight:600}
287
+ .btxt code{background:#222233;border:1px solid var(--bdr2);border-radius:4px;padding:1px 6px;font-family:'Courier New',monospace;font-size:12px;color:var(--acc-hi)}
288
+ .btxt pre{background:#1a1a2a;border:1px solid var(--bdr2);border-radius:8px;padding:13px;overflow-x:auto;margin:9px 0}
289
+ .btxt pre code{background:none;border:none;padding:0}
290
+ .btxt table{width:100%;border-collapse:collapse;margin:9px 0;font-size:13px;display:block;overflow-x:auto}
291
+ .btxt th{background:#1e1a3a;color:var(--acc-hi);font-weight:600;padding:8px 11px;border:1px solid var(--bdr2);white-space:nowrap}
292
+ .btxt td{padding:7px 11px;border:1px solid var(--bdr2);vertical-align:top}
293
+ .btxt tr:nth-child(even) td{background:rgba(255,255,255,.025)}
294
+ .btxt blockquote{border-left:3px solid var(--acc-hi);padding-left:13px;color:#aaa;margin:9px 0}
295
+
296
+ .tdots{display:flex;gap:5px;align-items:center;padding:4px 0}
297
+ .tdots span{width:8px;height:8px;border-radius:50%;background:var(--txt3);animation:tb 1.3s infinite ease-in-out}
298
+ .tdots span:nth-child(2){animation-delay:.2s}
299
+ .tdots span:nth-child(3){animation-delay:.4s}
300
+ @keyframes tb{0%,80%,100%{transform:translateY(0);background:var(--txt3)}40%{transform:translateY(-6px);background:var(--acc-hi)}}
301
+
302
+ #spin{
303
+ display:none;flex-shrink:0;
304
+ justify-content:center;align-items:center;gap:8px;
305
+ padding:7px;font-size:12px;color:var(--txt2);
306
+ }
307
+ .sring{
308
+ width:15px;height:15px;
309
+ border:2px solid var(--bdr2);border-top-color:var(--acc-hi);
310
+ border-radius:50%;animation:sr .7s linear infinite;flex-shrink:0;
311
+ }
312
+ @keyframes sr{to{transform:rotate(360deg)}}
313
+
314
+ #sbar{
315
+ display:none;flex-shrink:0;
316
+ padding:5px clamp(12px,3vw,20px);
317
+ background:var(--surf);border-top:1px solid var(--bdr);
318
+ font-size:11px;color:var(--txt2);
319
+ }
320
+
321
+ .macts{display:flex;gap:4px;margin-top:8px}
322
+ .abtn,.pbtn{
323
+ display:inline-flex;align-items:center;justify-content:center;
324
+ width:30px;height:30px;border-radius:6px;
325
+ border:1px solid #2e2e3d;background:transparent;
326
+ color:#555;cursor:pointer;
327
+ transition:border-color .15s,color .15s,background .15s;
328
+ padding:0;touch-action:manipulation;
329
+ }
330
+ .abtn:hover{border-color:var(--acc-hi);color:var(--acc-hi);background:var(--acc-glow)}
331
+ .abtn.copied{border-color:var(--green);color:var(--green)}
332
+ .pbtn{border-color:#1e2e24;color:#3a6a4a}
333
+ .pbtn:hover{border-color:var(--green);color:var(--green);background:rgba(34,216,122,.08)}
334
+
335
+ #welcome{
336
+ max-width:min(540px,100%);
337
+ margin:clamp(18px,5vw,52px) auto;
338
+ padding:0 clamp(14px,4vw,24px);
339
+ text-align:center;
340
+ }
341
+ #welcome .wi{font-size:clamp(32px,7vw,48px);margin-bottom:14px}
342
+ #welcome h2{font-family:var(--fd);font-size:clamp(15px,3.5vw,21px);font-weight:700;color:#fff;margin-bottom:10px}
343
+ #welcome p{font-size:clamp(12px,2.2vw,14px);color:var(--txt2);line-height:1.75}
344
+
345
+ #disc{
346
+ flex-shrink:0;text-align:center;
347
+ font-size:clamp(9px,1.6vw,11px);color:var(--txt3);
348
+ padding:4px clamp(8px,3vw,16px) clamp(5px,1.5vw,8px);
349
+ }
350
+
351
+ /* ─── RESPONSIVE ──────────────────────────────────────── */
352
+
353
+ /* MOBILE ≀ 820px */
354
+ @media(max-width:820px){
355
+ /* Show toggle bar and drawer */
356
+ #mob-toggle{display:flex}
357
+ #mob-drawer{display:block}
358
+
359
+ /* Hide desktop sidebar */
360
+ #form-panel{display:none!important}
361
+
362
+ /* Main only contains chat */
363
+ #main{flex-direction:column}
364
+ #chat-panel{flex:1;min-height:0}
365
+
366
+ .mrow{padding:0 13px}
367
+
368
+ /* Bigger tap targets & no iOS zoom */
369
+ .fld input,.fld select,.fld textarea{font-size:16px;padding:10px}
370
+ .sub-btn{padding:13px}
371
+ .abtn,.pbtn{width:36px;height:36px}
372
+ }
373
+
374
+ /* Very small phones */
375
+ @media(max-width:380px){
376
+ .hs{display:none}
377
+ #qpill{padding:3px 8px;font-size:10px}
378
+ }
379
+
380
+ /* Landscape small */
381
+ @media(max-height:480px) and (max-width:820px){
382
+ #mob-drawer.open{max-height:96dvh}
383
+ #welcome .wi{display:none}
384
+ }
385
+
386
+ /* Large desktops */
387
+ @media(min-width:1360px){
388
+ :root{--fw:350px}
389
+ .mrow{max-width:860px}
390
+ }
391
+ </style>
392
+ </head>
393
+ <body>
394
+
395
+ <!-- ── SVG sprite ─��� -->
396
+ <svg style="display:none">
397
+ <symbol id="ic-copy" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
398
+ <rect x="9" y="9" width="13" height="13" rx="2"/>
399
+ <path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/>
400
+ </symbol>
401
+ <symbol id="ic-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
402
+ <polyline points="20 6 9 17 4 12"/>
403
+ </symbol>
404
+ <symbol id="ic-dl" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
405
+ <path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/>
406
+ <polyline points="7 10 12 15 17 10"/>
407
+ <line x1="12" y1="15" x2="12" y2="3"/>
408
+ </symbol>
409
+ </svg>
410
+
411
+ <div id="app">
412
+
413
+ <!-- ── HEADER ── -->
414
+ <div id="hdr">
415
+ <div class="hl">
416
+ <div class="hi">πŸŽ“</div>
417
+ <div style="min-width:0">
418
+ <div class="ht">SmartGrad AI Advisor</div>
419
+ <div class="hs">Multi-agent university guidance</div>
420
+ </div>
421
+ </div>
422
+ <div class="hr">
423
+ <div id="qpill">2 / 2 today</div>
424
+ <div class="spill"><div class="sdot"></div><span>Connected</span></div>
425
+ </div>
426
+ </div>
427
+
428
+ <!-- ── MOBILE TOGGLE BAR (tap to open/close drawer) ── -->
429
+ <div id="mob-toggle" onclick="toggleDrawer()" role="button" aria-expanded="false" aria-controls="mob-drawer">
430
+ <div class="mt-left">
431
+ <div class="mt-icon">✏️</div>
432
+ <div>
433
+ <div class="mt-label">Student Profile</div>
434
+ <div class="mt-hint" id="mt-hint">Tap to fill in your details</div>
435
+ </div>
436
+ </div>
437
+ <span class="mt-arrow" id="mt-arrow">β–Ό</span>
438
+ </div>
439
+
440
+ <!-- ── MOBILE DRAWER (slide-down) ── -->
441
+ <div id="mob-drawer" role="region" aria-label="Student profile form">
442
+ <div id="mob-drawer-inner">
443
+ <div class="form-ttl">πŸŽ“ Your Details</div>
444
+ <!-- Fields injected here by JS -->
445
+ </div>
446
+ </div>
447
+
448
+ <!-- ── PROGRESS BAR ── -->
449
+ <div id="pbar"><div id="pfill"></div></div>
450
+
451
+ <!-- ── MAIN ── -->
452
+ <div id="main">
453
+
454
+ <!-- Desktop sidebar form (always visible β‰₯821px) -->
455
+ <div id="form-panel">
456
+ <div class="form-ttl">πŸŽ“ Student Profile</div>
457
+ <!-- Fields injected here by JS -->
458
+ </div>
459
+
460
+ <!-- Chat -->
461
+ <div id="chat-panel">
462
+ <div id="msgs-area">
463
+ <div id="welcome">
464
+ <div class="wi">πŸŽ“</div>
465
+ <h2>Welcome to SmartGrad</h2>
466
+ <p>
467
+ Fill in your student profile and click <strong>"Get Recommendations"</strong>.<br/><br/>
468
+ Our AI agents will research programmes, scholarships, visa requirements, and financial planning β€” then write a personalised advisory letter just for you.<br/><br/>
469
+ <span style="color:var(--yellow);font-size:12px">⚠ You have <strong id="q-remain">2</strong> free queries today.</span>
470
+ </p>
471
+ </div>
472
+ <div id="msgs"></div>
473
+ </div>
474
+ <div id="spin"><div class="sring"></div><span id="spin-lbl">Agents are working…</span></div>
475
+ <div id="sbar"><span id="stxt">⏳ Processing…</span></div>
476
+ <div id="disc">AI can make mistakes β€” verify with official university sources &nbsp;|&nbsp; 2 queries/day &nbsp;|&nbsp; Developed by Gacirane Patrick Β© 2026</div>
477
+ </div>
478
+
479
+ </div><!-- /#main -->
480
+ </div><!-- /#app -->
481
+
482
+ <script>
483
+ (function(){
484
+ 'use strict';
485
+
486
+ /* ═══════════════════════════════════════════════════
487
+ FIELD DEFINITIONS β€” single source of truth
488
+ ═══════════════════════════════════════════════════ */
489
+ var FIELDS = [
490
+ {id:'student_name', label:'Student Name *', type:'text', ph:'e.g. Jane Uwase', ac:'name'},
491
+ {id:'lower_level', label:'Current Education Level', type:'select', opts:[['high school','High School'],['college','College / Diploma'],['undergraduate','Undergraduate']]},
492
+ {id:'higher_level', label:'Desired Level *', type:'select', opts:[['undergraduate','Undergraduate'],['masters','Masters'],['phd','PhD'],['diploma','Diploma / Certificate']]},
493
+ {id:'grade', label:'Grade / Score *', type:'text', ph:'e.g. A (distinction, 87%)'},
494
+ {id:'major', label:'Major / Field of Study *', type:'text', ph:'e.g. Sciences (Math, Physics)'},
495
+ {id:'subjects', label:'Subjects Studied *', type:'textarea', ph:'e.g. Mathematics, Physics, Chemistry'},
496
+ {id:'program', label:'Desired Program *', type:'text', ph:'e.g. Computer Science / AI'},
497
+ {id:'country', label:'Home Country *', type:'text', ph:'e.g. Rwanda', ac:'country-name'},
498
+ {id:'destination_country', label:'Destination Country *', type:'text', ph:'e.g. Canada'},
499
+ {id:'state', label:'Province / State *', type:'text', ph:'e.g. Ontario'}
500
+ ];
501
+ var REQUIRED = ['student_name','grade','major','subjects','program','country','destination_country','state'];
502
+
503
+ /* Build fields HTML for a container, using an id prefix */
504
+ function buildFields(prefix){
505
+ return FIELDS.map(function(f){
506
+ var id = prefix+f.id;
507
+ var ctrl = '';
508
+ if(f.type==='select'){
509
+ ctrl = '<select id="'+id+'">'
510
+ + f.opts.map(function(o){return '<option value="'+o[0]+'">'+o[1]+'</option>';}).join('')
511
+ + '</select>';
512
+ } else if(f.type==='textarea'){
513
+ ctrl = '<textarea id="'+id+'" rows="2" placeholder="'+f.ph+'"></textarea>';
514
+ } else {
515
+ ctrl = '<input type="text" id="'+id+'" placeholder="'+f.ph+'"'+(f.ac?' autocomplete="'+f.ac+'"':'')+'/>';
516
+ }
517
+ return '<div class="fld"><label for="'+id+'">'+f.label+'</label>'+ctrl+'</div>';
518
+ }).join('');
519
+ }
520
+
521
+ /* Inject fields into containers */
522
+ var desktopPanel = document.getElementById('form-panel');
523
+ var drawerInner = document.getElementById('mob-drawer-inner');
524
+
525
+ /* Desktop: append fields + submit btn + quota warn */
526
+ desktopPanel.insertAdjacentHTML('beforeend',
527
+ buildFields('d-')
528
+ +'<button class="sub-btn" id="d-sub" onclick="doSubmit()">πŸš€ Get Recommendations</button>'
529
+ +'<div class="qwarn" id="d-qwarn"></div>'
530
+ );
531
+
532
+ /* Mobile drawer: append fields + submit btn + quota warn */
533
+ drawerInner.insertAdjacentHTML('beforeend',
534
+ buildFields('m-')
535
+ +'<button class="sub-btn" id="m-sub" onclick="doSubmit()">πŸš€ Get Recommendations</button>'
536
+ +'<div class="qwarn" id="m-qwarn"></div>'
537
+ );
538
+
539
+ /* ═══════════════════════════════════════════════════
540
+ QUOTA (localStorage, resets daily)
541
+ ═══════════════════════════════════════════════════ */
542
+ var QKEY = 'sg_q';
543
+ var QLIM = 2;
544
+
545
+ function today(){ var d=new Date(); return d.getFullYear()+'-'+(d.getMonth()+1)+'-'+d.getDate(); }
546
+ function getQ(){
547
+ try{ var r=localStorage.getItem(QKEY); if(r){var o=JSON.parse(r);if(o.d===today())return o.c;} }catch(e){}
548
+ return 0;
549
+ }
550
+ function incQ(){ try{ localStorage.setItem(QKEY,JSON.stringify({d:today(),c:getQ()+1})); }catch(e){} }
551
+ function rem(){ return Math.max(0,QLIM-getQ()); }
552
+
553
+ function refreshQuotaUI(){
554
+ var r = rem();
555
+ var pill = document.getElementById('qpill');
556
+ var qr = document.getElementById('q-remain');
557
+ if(pill){ pill.textContent=r+' / '+QLIM+' today'; pill.className=r===0?'full':r===1?'warn':''; }
558
+ if(qr) qr.textContent=r;
559
+
560
+ ['d','m'].forEach(function(p){
561
+ var w = document.getElementById(p+'-qwarn');
562
+ var b = document.getElementById(p+'-sub');
563
+ if(!w||!b) return;
564
+ if(r===0){
565
+ w.className='qwarn lim'; w.style.display='block';
566
+ w.innerHTML='🚫 Daily limit reached.<br>Come back tomorrow.';
567
+ b.disabled=true;
568
+ } else if(r===1){
569
+ w.className='qwarn one'; w.style.display='block';
570
+ w.innerHTML='⚠ Last query remaining today.';
571
+ b.disabled=false;
572
+ } else {
573
+ w.style.display='none'; b.disabled=false;
574
+ }
575
+ });
576
+ }
577
+
578
+ /* ═══════════════════════════════════════════════════
579
+ MOBILE DRAWER LOGIC
580
+ ═══════════════════════════════════════════════════ */
581
+ var drawer = document.getElementById('mob-drawer');
582
+ var toggle = document.getElementById('mob-toggle');
583
+ var mtArrow = document.getElementById('mt-arrow');
584
+ var mtHint = document.getElementById('mt-hint');
585
+ var isOpen = false;
586
+
587
+ function openDrawer(){
588
+ isOpen = true;
589
+ drawer.classList.add('open');
590
+ mtArrow.classList.add('open');
591
+ toggle.setAttribute('aria-expanded','true');
592
+ mtHint.textContent = 'Tap to collapse';
593
+ /* Sync desktop β†’ mobile values so user doesn't lose data */
594
+ syncFields('d-','m-');
595
+ }
596
+
597
+ function closeDrawer(){
598
+ isOpen = false;
599
+ drawer.classList.remove('open');
600
+ mtArrow.classList.remove('open');
601
+ toggle.setAttribute('aria-expanded','false');
602
+ /* Update hint: show summary if fields are filled */
603
+ var name = fieldVal('student_name');
604
+ var prog = fieldVal('program');
605
+ mtHint.textContent = (name && prog)
606
+ ? 'βœ“ '+name+' Β· '+prog+' β€” tap to edit'
607
+ : 'Tap to fill in your details';
608
+ }
609
+
610
+ window.toggleDrawer = function(){
611
+ if(isOpen) closeDrawer(); else openDrawer();
612
+ };
613
+
614
+ /* Touch: swipe up on drawer header collapses it */
615
+ var ty0 = 0;
616
+ toggle.addEventListener('touchstart',function(e){ty0=e.touches[0].clientY;},{passive:true});
617
+ toggle.addEventListener('touchend',function(e){
618
+ if(isOpen && (ty0 - e.changedTouches[0].clientY) > 40){ closeDrawer(); e.preventDefault(); }
619
+ },{passive:false});
620
+
621
+ /* ═══════════════════════════════════════════════════
622
+ FIELD VALUE HELPERS
623
+ ════════════��══════════════════════════════════════ */
624
+ function isMob(){ return window.matchMedia('(max-width:820px)').matches; }
625
+
626
+ /* Get value from whichever context is active */
627
+ function fieldVal(name){
628
+ var p = isMob() ? 'm-' : 'd-';
629
+ var el = document.getElementById(p+name);
630
+ return el ? el.value.trim() : '';
631
+ }
632
+
633
+ /* Copy all field values from one prefix to another */
634
+ function syncFields(from, to){
635
+ FIELDS.forEach(function(f){
636
+ var s=document.getElementById(from+f.id);
637
+ var d=document.getElementById(to+f.id);
638
+ if(s&&d) d.value=s.value;
639
+ });
640
+ }
641
+
642
+ /* ═══════════════════════════════════════════════════
643
+ VALIDATION
644
+ ═══════════════════════════════════════════════════ */
645
+ function validate(){
646
+ var p = isMob() ? 'm-' : 'd-';
647
+ var ok=true;
648
+ REQUIRED.forEach(function(name){
649
+ var el=document.getElementById(p+name);
650
+ if(!el||!el.value.trim()){
651
+ el.classList.add('invalid');
652
+ if(ok){ el.focus(); ok=false; }
653
+ el.addEventListener('input',function h(){ el.classList.remove('invalid'); el.removeEventListener('input',h); });
654
+ } else {
655
+ el.classList.remove('invalid');
656
+ }
657
+ });
658
+ /* On mobile, open drawer so errors are visible */
659
+ if(!ok && isMob() && !isOpen) openDrawer();
660
+ return ok;
661
+ }
662
+
663
+ /* ═══════════════════════════════════════════════════
664
+ SUBMIT
665
+ ═══════════════════════════════════════════════════ */
666
+ var CTX = '<%= ctxPath %>';
667
+ var msgs = document.getElementById('msgs');
668
+ var spin = document.getElementById('spin');
669
+ var slbl = document.getElementById('spin-lbl');
670
+ var sbar = document.getElementById('sbar');
671
+ var stxt = document.getElementById('stxt');
672
+ var pbar = document.getElementById('pbar');
673
+ var pfill = document.getElementById('pfill');
674
+ var evSrc = null;
675
+
676
+ window.doSubmit = function(){
677
+ if(rem()<=0){
678
+ alert('You have reached your daily limit of '+QLIM+' queries. Please come back tomorrow.');
679
+ return;
680
+ }
681
+ if(!validate()) return;
682
+
683
+ /* Sync fields between panels before reading values */
684
+ if(isMob()) syncFields('m-','d-'); else syncFields('d-','m-');
685
+
686
+ /* ── Auto-collapse drawer after submit ── */
687
+ if(isMob()) closeDrawer();
688
+
689
+ /* Hide welcome screen */
690
+ var wel=document.getElementById('welcome');
691
+ if(wel) wel.style.display='none';
692
+
693
+ /* User bubble summary */
694
+ var name = fieldVal('student_name');
695
+ var prog = fieldVal('program');
696
+ var dest = fieldVal('destination_country');
697
+ addUserBubble('πŸŽ“ '+name+' β€” '+prog+' in '+dest);
698
+ var botTxt = addBotBubble();
699
+
700
+ /* Record quota use BEFORE request (prevents refresh bypass) */
701
+ incQ();
702
+ refreshQuotaUI();
703
+
704
+ /* Disable both submit buttons while processing */
705
+ setSubmitBtns(true);
706
+
707
+ /* Show progress indicators */
708
+ spin.style.display = 'flex';
709
+ slbl.textContent = 'Connecting to advisory system…';
710
+ pbar.style.display = 'block';
711
+ pfill.style.width = '5%';
712
+ sbar.style.display = 'block';
713
+ stxt.textContent = '⏳ Submitting your profile…';
714
+ scrollDown();
715
+
716
+ if(evSrc) evSrc.close();
717
+
718
+ var p = new URLSearchParams({
719
+ student_name: fieldVal('student_name'),
720
+ lower_level: fieldVal('lower_level'),
721
+ higher_level: fieldVal('higher_level'),
722
+ grade: fieldVal('grade'),
723
+ major: fieldVal('major'),
724
+ subjects: fieldVal('subjects'),
725
+ program: fieldVal('program'),
726
+ country: fieldVal('country'),
727
+ destination_country: fieldVal('destination_country'),
728
+ state: fieldVal('state'),
729
+ agent_name: 'SmartGrad AI Advisor',
730
+ institution: 'SmartGrad Education Services'
731
+ });
732
+
733
+ evSrc = new EventSource(CTX+'/CrewResponseStream?'+p.toString());
734
+ var first=true, buf='', pct=10;
735
+
736
+ evSrc.onmessage = function(e){
737
+ var data = e.data;
738
+ if(data.trim()==='[END]'){
739
+ evSrc.close();
740
+ spin.style.display='none'; pbar.style.display='none'; sbar.style.display='none';
741
+ pfill.style.width='100%';
742
+ if(rem()>0) setSubmitBtns(false);
743
+ if(buf){ botTxt.innerHTML=marked.parse(buf); addActions(botTxt.parentElement,buf); }
744
+ scrollDown(); return;
745
+ }
746
+ if(data.startsWith('STATUS:')){
747
+ var m=data.substring(7).trim();
748
+ stxt.textContent='⏳ '+m; slbl.textContent=m;
749
+ pct=Math.min(pct+10,90); pfill.style.width=pct+'%'; return;
750
+ }
751
+ var tr=data.trim();
752
+ if(tr.startsWith('⏳')||tr.startsWith('πŸ”„')||(tr.startsWith('βœ…')&&data.length<60)) return;
753
+ if(first){botTxt.innerHTML='';first=false;}
754
+ buf+='\n'+data;
755
+ botTxt.innerHTML=data.length>200?marked.parse(buf):botTxt.innerHTML+marked.parse(data);
756
+ scrollDown();
757
+ };
758
+
759
+ evSrc.onerror=function(){
760
+ spin.style.display='none'; pbar.style.display='none'; sbar.style.display='none';
761
+ if(rem()>0) setSubmitBtns(false);
762
+ botTxt.innerHTML='<span style="color:var(--red)">⚠ Connection error. Please try again.</span>';
763
+ evSrc.close(); scrollDown();
764
+ };
765
+ };
766
+
767
+ function setSubmitBtns(disabled){
768
+ var ds=document.getElementById('d-sub');
769
+ var ms=document.getElementById('m-sub');
770
+ if(ds) ds.disabled=disabled;
771
+ if(ms) ms.disabled=disabled;
772
+ }
773
+
774
+ /* ═══════════════════════════════════════════════════
775
+ DOM / CHAT HELPERS
776
+ ═══════════════════════════════════════════════════ */
777
+ function addUserBubble(text){
778
+ var row=document.createElement('div'); row.className='mrow';
779
+ row.innerHTML='<div class="urow"><div class="ubub"></div></div>';
780
+ row.querySelector('.ubub').textContent=text;
781
+ msgs.appendChild(row);
782
+ }
783
+
784
+ function addBotBubble(){
785
+ var row=document.createElement('div'); row.className='mrow';
786
+ row.innerHTML=
787
+ '<div class="brow">'
788
+ +'<div class="bav">πŸŽ“</div>'
789
+ +'<div class="bcon">'
790
+ +'<div class="bname">SmartGrad Advisor &nbsp;<span style="font-size:11px;color:var(--txt3)">'+fmtT()+'</span></div>'
791
+ +'<div class="btxt"><div class="tdots"><span></span><span></span><span></span></div></div>'
792
+ +'</div></div>';
793
+ msgs.appendChild(row);
794
+ return row.querySelector('.btxt');
795
+ }
796
+
797
+ function addActions(container, md){
798
+ var bar=document.createElement('div'); bar.className='macts';
799
+ var cp=document.createElement('button');
800
+ cp.className='abtn'; cp.setAttribute('aria-label','Copy');
801
+ cp.innerHTML=svg('ic-copy',14);
802
+ cp.onclick=function(){
803
+ navigator.clipboard.writeText(md).then(function(){
804
+ cp.innerHTML=svg('ic-check',14); cp.classList.add('copied');
805
+ setTimeout(function(){cp.innerHTML=svg('ic-copy',14);cp.classList.remove('copied');},2000);
806
+ });
807
+ };
808
+ bar.appendChild(cp);
809
+ if(md.length>300){
810
+ var pd=document.createElement('button');
811
+ pd.className='pbtn'; pd.setAttribute('aria-label','Download PDF');
812
+ pd.innerHTML=svg('ic-dl',14);
813
+ pd.onclick=function(){makePdf(md);};
814
+ bar.appendChild(pd);
815
+ }
816
+ var bc=container.querySelector('.bcon')||container;
817
+ bc.appendChild(bar);
818
+ }
819
+
820
+ function scrollDown(){
821
+ requestAnimationFrame(function(){
822
+ var a=document.getElementById('msgs-area');
823
+ a.scrollTop=a.scrollHeight;
824
+ });
825
+ }
826
+ function fmtT(){ return new Date().toLocaleTimeString([],{hour:'2-digit',minute:'2-digit'}); }
827
+ function svg(id,sz){ return '<svg width="'+sz+'" height="'+sz+'" aria-hidden="true"><use href="#'+id+'"/></svg>'; }
828
+
829
+ /* ═══════════════════════════════════════════════════
830
+ PDF EXPORT
831
+ ═══════════════════════════════════════════════════ */
832
+ function makePdf(md){
833
+ var J=window.jspdf?window.jspdf.jsPDF:null;
834
+ if(!J){alert('jsPDF not loaded');return;}
835
+ var doc=new J(), lines=md.split('\n');
836
+ var mX=15,mT=28,mB=22,pH=doc.internal.pageSize.height,y=mT,pg=1;
837
+ var now=new Date().toLocaleString();
838
+ function hdr(){
839
+ doc.setFont('Helvetica','bold');doc.setFontSize(13);doc.setTextColor(80,20,160);
840
+ doc.text('SmartGrad β€” University Advisory Report',mX,12);
841
+ doc.setDrawColor(80,20,160);doc.line(mX,15,195,15);
842
+ doc.setFont('Helvetica','normal');doc.setFontSize(9);doc.setTextColor(120);
843
+ doc.text('Confidential Β· Generated by SmartGrad AI Advisor Β· '+now,mX,21);
844
+ }
845
+ function ftr(){
846
+ doc.setFont('Helvetica','normal');doc.setFontSize(9);doc.setTextColor(130);
847
+ doc.line(mX,pH-14,195,pH-14);
848
+ doc.text('Page '+pg+' | SmartGrad Education Services | '+now,mX,pH-8);
849
+ }
850
+ function wln(t){
851
+ doc.splitTextToSize(t,180).forEach(function(ln){
852
+ if(y>pH-mB-10){ftr();doc.addPage();pg++;hdr();y=mT;}
853
+ doc.setFont('Helvetica','normal');doc.setFontSize(10.5);doc.setTextColor(30);
854
+ doc.text(ln,mX,y);y+=6.5;
855
+ });y+=2;
856
+ }
857
+ hdr();lines.forEach(wln);ftr();
858
+ doc.save('SmartGrad_Advisory_'+Date.now()+'.pdf');
859
+ }
860
+
861
+ /* ── Init ── */
862
+ refreshQuotaUI();
863
+
864
+ })();
865
+ </script>
866
+ </body>
867
+ </html>
src/main/webapp/WEB-INF/web.xml ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
3
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4
+ xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
5
+ version="4.0">
6
+ <session-config>
7
+ <session-timeout>
8
+ 30
9
+ </session-timeout>
10
+ </session-config>
11
+ </web-app>
src/main/webapp/index.html ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>Start Page</title>
5
+ <meta http-equiv="refresh" content="0; URL='chat'" />
6
+ </head>
7
+ <body>
8
+ <h1>Welcome to Smart Grad!</h1>
9
+ </body>
10
+ </html>