github-actions[bot] commited on
Commit
5a44b8b
·
0 Parent(s):

Space deploy: orphan commit with Docker config + LFS pointers

Browse files
.gitattributes ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ screenshots/*.png filter=lfs diff=lfs merge=lfs -text
2
+ *.gif filter=lfs diff=lfs merge=lfs -text
3
+ *.webp filter=lfs diff=lfs merge=lfs -text
4
+ *.zip filter=lfs diff=lfs merge=lfs -text
5
+ *.jar filter=lfs diff=lfs merge=lfs -text
6
+ *.png filter=lfs diff=lfs merge=lfs -text
7
+ *.jpg filter=lfs diff=lfs merge=lfs -text
8
+ *.jpeg filter=lfs diff=lfs merge=lfs -text
.github/workflows/deploy-to-hf-space.yml ADDED
@@ -0,0 +1,113 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Deploy to Hugging Face Space (Docker)
2
+
3
+ on:
4
+ push:
5
+ branches: [ master ] # change if your default branch differs
6
+ workflow_dispatch:
7
+
8
+ jobs:
9
+ deploy:
10
+ name: Create Docker Space + push orphan LFS deploy
11
+ runs-on: ubuntu-latest
12
+
13
+ defaults:
14
+ run:
15
+ shell: bash
16
+
17
+ env:
18
+ HF_TOKEN: ${{ secrets.HF_TOKEN }} # HF token with write access
19
+ HF_USERNAME: Phoenix21 # your HF username/org
20
+ HF_SPACE: SpringGeminiChat # Space name
21
+
22
+ steps:
23
+ # 0) Checkout repo (full history; not shallow)
24
+ - uses: actions/checkout@v4
25
+ with:
26
+ fetch-depth: 0
27
+ lfs: false
28
+
29
+ # 1) Install hub client + Git LFS
30
+ - name: Install hub client + git-lfs
31
+ run: |
32
+ set -euo pipefail
33
+ python -m pip install --upgrade pip
34
+ python -m pip install "huggingface_hub>=0.23"
35
+ sudo apt-get update
36
+ sudo apt-get install -y git-lfs
37
+ git lfs install
38
+
39
+ # 2) Create Space if missing (SDK = docker)
40
+ - name: Create Space if missing (SDK = docker)
41
+ shell: python
42
+ env:
43
+ HF_TOKEN: ${{ env.HF_TOKEN }}
44
+ HF_USERNAME: ${{ env.HF_USERNAME }}
45
+ HF_SPACE: ${{ env.HF_SPACE }}
46
+ run: |
47
+ import os
48
+ from huggingface_hub import HfApi
49
+ api = HfApi(token=os.environ["HF_TOKEN"])
50
+ repo_id = f"{os.environ['HF_USERNAME']}/{os.environ['HF_SPACE']}"
51
+ try:
52
+ api.repo_info(repo_id, repo_type="space")
53
+ print(f"Space exists: {repo_id}")
54
+ except Exception:
55
+ api.create_repo(
56
+ repo_id=repo_id,
57
+ repo_type="space",
58
+ space_sdk="docker", # ensure a Docker Space
59
+ private=False
60
+ )
61
+ print(f"Space created: {repo_id}")
62
+
63
+ # 3) Build an orphan deploy snapshot with Docker README + LFS
64
+ - name: Prepare orphan deploy commit (no heredoc; Docker README + LFS)
65
+ run: |
66
+ set -euo pipefail
67
+
68
+ # Orphan branch so Space won't inherit non-LFS history
69
+ git checkout --orphan space-deploy
70
+
71
+ # Ensure README has Docker Space YAML (sdk + port) WITHOUT heredoc
72
+ if ! grep -qE '^sdk:\s*docker' README.md 2>/dev/null; then
73
+ tmp="$(mktemp)"
74
+ printf '%s\n' '---' 'title: Spring Gemini Chat (Docker)' 'sdk: docker' 'app_port: 7860' '---' '' > "$tmp"
75
+ [ -f README.md ] && cat README.md >> "$tmp" || true
76
+ mv "$tmp" README.md
77
+ fi
78
+
79
+ # Track images/binaries with LFS (HF requires LFS for binaries)
80
+ git lfs track "screenshots/*.png"
81
+ git lfs track "*.png" "*.jpg" "*.jpeg" "*.gif" "*.webp" "*.zip" "*.jar"
82
+ git add .gitattributes
83
+
84
+ # Stage EVERYTHING so the Space receives your full repo snapshot
85
+ git add -A .
86
+
87
+ # Optional diagnostics
88
+ echo "---- git status ----"; git status -s || true
89
+ echo "---- sample files ----"; git ls-files | head -n 50 || true
90
+ echo "---- LFS pointers ----"; git lfs ls-files || true
91
+
92
+ # Ignore build outputs
93
+ printf '%s\n' "target/" "*.log" >> .gitignore
94
+ git add .gitignore
95
+
96
+ # Commit the deploy snapshot
97
+ git config user.name "github-actions[bot]"
98
+ git config user.email "github-actions[bot]@users.noreply.github.com"
99
+ git commit -m "Space deploy: orphan commit with Docker config + LFS pointers"
100
+ git log --oneline -1
101
+
102
+ # 4) Push to Space (force first sync) and verify remote SHA
103
+ - name: Push to Space (force first sync + verify)
104
+ run: |
105
+ set -euo pipefail
106
+ SPACE_PATH="${HF_USERNAME}/${HF_SPACE}"
107
+ git remote remove space 2>/dev/null || true
108
+ git remote add space "https://${HF_USERNAME}:${HF_TOKEN}@huggingface.co/spaces/${SPACE_PATH}"
109
+
110
+ echo "Local HEAD before push:"; git rev-parse HEAD
111
+ git push --force space space-deploy:main
112
+ echo "Remote main after push (new SHA expected):"
113
+ git ls-remote --heads space refs/heads/main || true
.gitignore ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ HELP.md
2
+ target/
3
+ .mvn/wrapper/maven-wrapper.jar
4
+ !**/src/main/**/target/
5
+ !**/src/test/**/target/
6
+
7
+ ### STS ###
8
+ .apt_generated
9
+ .classpath
10
+ .factorypath
11
+ .project
12
+ .settings
13
+ .springBeans
14
+ .sts4-cache
15
+
16
+ ### IntelliJ IDEA ###
17
+ .idea
18
+ *.iws
19
+ *.iml
20
+ *.ipr
21
+
22
+ ### NetBeans ###
23
+ /nbproject/private/
24
+ /nbbuild/
25
+ /dist/
26
+ /nbdist/
27
+ /.nb-gradle/
28
+ build/
29
+ !**/src/main/**/build/
30
+ !**/src/test/**/build/
31
+ !**/src/main/resources/application.properties
32
+ ### VS Code ###
33
+ .vscode/
34
+ target/
35
+ *.log
Dockerfile ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # syntax=docker/dockerfile:1
2
+
3
+ ############################
4
+ # 1) Build stage (Maven)
5
+ ############################
6
+ FROM maven:3.9-eclipse-temurin-17 AS build
7
+ WORKDIR /workspace
8
+
9
+ # Copy only pom first to warm dependency cache
10
+ COPY pom.xml .
11
+ RUN mvn -B -q -DskipTests dependency:go-offline
12
+
13
+ # Now copy sources and build
14
+ COPY src ./src
15
+ RUN mvn -B -DskipTests package
16
+
17
+ ############################
18
+ # 2) Runtime stage (JRE)
19
+ ############################
20
+ FROM eclipse-temurin:17-jre-jammy
21
+
22
+ # Hugging Face Spaces containers run with UID 1000; create matching user to avoid permission issues
23
+ # (Recommended in Docker Spaces docs)
24
+ RUN useradd -m -u 1000 appuser
25
+ USER appuser
26
+ WORKDIR /home/appuser
27
+
28
+ # Bring in built jar
29
+ COPY --from=build /workspace/target/*.jar app.jar
30
+
31
+ # Default port Spaces expose is 7860; the README's app_port can change it.
32
+ # We'll respect $PORT if Spaces set it; otherwise default to 7860.
33
+ ENV PORT=7860
34
+ ENV JAVA_OPTS=""
35
+
36
+ # Optional: Expose for local runs
37
+ EXPOSE 7860
38
+
39
+ # Run Spring on the Space port, bind to 0.0.0.0 so the Space can reach it
40
+ ENTRYPOINT ["sh","-c","java $JAVA_OPTS -jar /home/appuser/app.jar --server.port=${PORT} --server.address=0.0.0.0"]
README.md ADDED
@@ -0,0 +1,337 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: SpringGeminiChat
3
+ emoji: 🏢
4
+ colorFrom: purple
5
+ colorTo: yellow
6
+ sdk: docker
7
+ app_port: 7860
8
+ pinned: false
9
+ ---
10
+
11
+ # 🤖 Spring Gemini Chat
12
+
13
+ A modern, reactive Spring Boot application that integrates with Google's Gemini AI to provide intelligent chat capabilities. Built with Spring WebFlux for high-performance, non-blocking operations and featuring a clean, responsive web interface.
14
+
15
+ ## 📸 Screenshots
16
+
17
+ ### Chat Interface
18
+ ![Chat Interface](screenshots/chat-interface.png)
19
+ *Modern, responsive chat UI with real-time messaging*
20
+
21
+ ### API Response Example
22
+ ![API Response](screenshots/api-response.png)
23
+ *JSON response structure from the Gemini integration*
24
+
25
+ ### Health Check Dashboard
26
+ ![Health Check](screenshots/health-check.png)
27
+ *Backend status and monitoring*
28
+
29
+ ---
30
+
31
+ ## ✨ Features
32
+
33
+ - 🚀 **Reactive Programming** - Built with Spring WebFlux for non-blocking operations
34
+ - 🤖 **Gemini AI Integration** - Powered by Google's latest Gemini AI model
35
+ - 💬 **Real-time Chat** - Instant responses with typing indicators
36
+ - 🎨 **Modern UI** - Clean, responsive web interface
37
+ - 📱 **Mobile Friendly** - Works seamlessly on all devices
38
+ - 🔄 **Health Monitoring** - Built-in health checks and status indicators
39
+ - 🛡️ **Error Handling** - Comprehensive error handling and validation
40
+ - ⚡ **High Performance** - Optimized for speed and scalability
41
+ - 🔧 **Easy Configuration** - Environment-based configuration
42
+ - 📊 **Conversation Context** - Support for contextual conversations
43
+
44
+ ## 🛠️ Technology Stack
45
+
46
+ ### Backend
47
+ - **Java 17** - Programming language
48
+ - **Spring Boot 3.1.5** - Application framework
49
+ - **Spring WebFlux** - Reactive programming
50
+ - **Spring Validation** - Input validation
51
+ - **Maven** - Dependency management
52
+ - **Google Gemini AI** - AI language model
53
+
54
+ ### Frontend
55
+ - **HTML5** - Structure
56
+ - **CSS3** - Styling with modern animations
57
+ - **Vanilla JavaScript** - Interactive functionality
58
+ - **Fetch API** - HTTP requests
59
+
60
+ ## 📋 Prerequisites
61
+
62
+ Before you begin, ensure you have the following installed:
63
+
64
+ - ☑️ **Java 17 or higher**
65
+ - ☑️ **Maven 3.6+**
66
+ - ☑️ **Git**
67
+ - ☑️ **Google Gemini API Key** ([Get it here](https://makersuite.google.com/app/apikey))
68
+
69
+ ## 🚀 Quick Start
70
+
71
+ ### 1. Clone the Repository
72
+ ```bash
73
+ git clone https://github.com/yourusername/spring-gemini-chat.git
74
+ cd spring-gemini-chat
75
+ ```
76
+
77
+ ### 2. Set Environment Variables
78
+ ```bash
79
+ # Windows
80
+ set GEMINI_API_KEY=your_actual_gemini_api_key_here
81
+
82
+ # macOS/Linux
83
+ export GEMINI_API_KEY=your_actual_gemini_api_key_here
84
+ ```
85
+
86
+ ### 3. Build and Run
87
+ ```bash
88
+ # Clean and install dependencies
89
+ mvn clean install
90
+
91
+ # Run the application
92
+ mvn spring-boot:run
93
+ ```
94
+
95
+ ### 4. Access the Application
96
+ - **Backend API**: http://localhost:8080
97
+ - **Health Check**: http://localhost:8080/api/chat/health
98
+ - **Frontend UI**: Open `chat-ui.html` in your browser
99
+
100
+ ## 📚 API Documentation
101
+
102
+ ### Base URL
103
+ ```
104
+ http://localhost:8080/api/chat
105
+ ```
106
+
107
+ ### Endpoints
108
+
109
+ #### 💬 Send Message
110
+ ```http
111
+ POST /message
112
+ Content-Type: application/json
113
+
114
+ {
115
+ "message": "Hello, how are you?"
116
+ }
117
+ ```
118
+
119
+ **Response:**
120
+ ```json
121
+ {
122
+ "response": "I'm doing well, thank you for asking! How can I help you today?",
123
+ "success": true,
124
+ "error": null,
125
+ "timestamp": 1692994488297
126
+ }
127
+ ```
128
+
129
+ #### 🗣️ Conversation with Context
130
+ ```http
131
+ POST /conversation
132
+ Content-Type: application/json
133
+
134
+ {
135
+ "messages": [
136
+ {
137
+ "role": "user",
138
+ "content": "What is Spring Boot?"
139
+ },
140
+ {
141
+ "role": "assistant",
142
+ "content": "Spring Boot is a Java framework..."
143
+ },
144
+ {
145
+ "role": "user",
146
+ "content": "Can you give me an example?"
147
+ }
148
+ ]
149
+ }
150
+ ```
151
+
152
+ #### 📡 Streaming Response
153
+ ```http
154
+ POST /stream
155
+ Content-Type: application/json
156
+
157
+ {
158
+ "message": "Tell me a story"
159
+ }
160
+ ```
161
+
162
+ #### 🔍 Health Check
163
+ ```http
164
+ GET /health
165
+ ```
166
+
167
+ **Response:**
168
+ ```json
169
+ {
170
+ "status": "UP",
171
+ "service": "Chat Service",
172
+ "timestamp": 1692994488297
173
+ }
174
+ ```
175
+
176
+ ## ⚙️ Configuration
177
+
178
+ ### Application Properties
179
+ ```properties
180
+ # Server Configuration
181
+ server.port=8080
182
+
183
+ # Gemini AI Configuration
184
+ gemini.api.key=${GEMINI_API_KEY:your-api-key-here}
185
+ gemini.api.base-url=https://generativelanguage.googleapis.com/v1beta/models
186
+ gemini.api.model=gemini-1.5-flash
187
+ gemini.api.timeout=30000
188
+
189
+ # CORS Configuration
190
+ cors.allowed-origins=http://localhost:3000,http://localhost:8080
191
+ ```
192
+
193
+ ### Environment Variables
194
+ | Variable | Description | Required | Default |
195
+ |----------|-------------|----------|---------|
196
+ | `GEMINI_API_KEY` | Your Google Gemini API key | ✅ Yes | - |
197
+ | `SERVER_PORT` | Server port | ❌ No | 8080 |
198
+ | `GEMINI_MODEL` | Gemini model to use | ❌ No | gemini-1.5-flash |
199
+
200
+ ## 🧪 Testing
201
+
202
+ ### Run Unit Tests
203
+ ```bash
204
+ mvn test
205
+ ```
206
+
207
+ ### Run Integration Tests
208
+ ```bash
209
+ mvn verify
210
+ ```
211
+
212
+ ### Manual Testing with cURL
213
+
214
+ **Windows Command Prompt:**
215
+ ```cmd
216
+ curl -X POST http://localhost:8080/api/chat/message -H "Content-Type: application/json" -d "{\"message\": \"Hello!\"}"
217
+ ```
218
+
219
+ **PowerShell/Linux/macOS:**
220
+ ```bash
221
+ curl -X POST http://localhost:8080/api/chat/message \
222
+ -H "Content-Type: application/json" \
223
+ -d '{"message": "Hello!"}'
224
+ ```
225
+
226
+ ## 🏗️ Project Structure
227
+
228
+ ```
229
+ spring-gemini-chat/
230
+ ├── src/
231
+ │ ├── main/
232
+ │ │ ├── java/com/example/p1/
233
+ │ │ │ ├── P1Application.java # Main application
234
+ │ │ │ ├── config/
235
+ │ │ │ │ └── AiConfiguration.java # Configuration
236
+ │ │ │ ├── controller/
237
+ │ │ │ │ └── ChatController.java # REST endpoints
238
+ │ │ │ └── service/
239
+ │ │ │ └── ChatService.java # Business logic
240
+ │ │ └── resources/
241
+ │ │ └── application.properties # Configuration
242
+ │ └── test/
243
+ │ └── java/com/example/p1/
244
+ │ └── P1ApplicationTests.java
245
+ ├── index.html # Frontend interface
246
+ ├── screenshots/ # UI screenshots
247
+ ├── pom.xml # Maven configuration
248
+ └── README.md # This file
249
+ ```
250
+
251
+ ## 🚀 Deployment
252
+
253
+ ### Using Docker
254
+ ```dockerfile
255
+ FROM openjdk:17-jdk-slim
256
+ COPY target/*.jar app.jar
257
+ EXPOSE 8080
258
+ ENTRYPOINT ["java","-jar","/app.jar"]
259
+ ```
260
+
261
+ ```bash
262
+ # Build the application
263
+ mvn clean package
264
+
265
+ # Build Docker image
266
+ docker build -t spring-gemini-chat .
267
+
268
+ # Run container
269
+ docker run -p 8080:8080 -e GEMINI_API_KEY=your_key spring-gemini-chat
270
+ ```
271
+
272
+ ### Using JAR
273
+ ```bash
274
+ # Build JAR
275
+ mvn clean package
276
+
277
+ # Run JAR
278
+ java -jar target/p1-0.0.1-SNAPSHOT.jar
279
+ ```
280
+
281
+ ## 🤝 Contributing
282
+
283
+ We welcome contributions! Please follow these steps:
284
+
285
+ 1. **Fork the repository**
286
+ 2. **Create a feature branch** (`git checkout -b feature/amazing-feature`)
287
+ 3. **Commit your changes** (`git commit -m 'Add some amazing feature'`)
288
+ 4. **Push to the branch** (`git push origin feature/amazing-feature`)
289
+ 5. **Open a Pull Request**
290
+
291
+ ### Development Guidelines
292
+ - Follow Java coding conventions
293
+ - Write unit tests for new features
294
+ - Update documentation for API changes
295
+ - Use meaningful commit messages
296
+
297
+ ## 🐛 Troubleshooting
298
+
299
+ ### Common Issues
300
+
301
+ **❌ "Connection failed" in UI**
302
+ - Ensure backend is running on port 8080
303
+ - Check CORS configuration
304
+ - Verify network connectivity
305
+
306
+ **❌ "API key not found" error**
307
+ - Set GEMINI_API_KEY environment variable
308
+ - Restart the application after setting the key
309
+ - Verify the API key is valid
310
+
311
+ **❌ Maven build fails**
312
+ - Check Java version (requires Java 17+)
313
+ - Clear Maven cache: `mvn clean`
314
+ - Update dependencies: `mvn dependency:resolve -U`
315
+
316
+ **❌ CORS errors**
317
+ - Check allowed origins in `AiConfiguration.java`
318
+ - Verify frontend URL matches CORS settings
319
+
320
+ ## 📄 License
321
+
322
+ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
323
+
324
+ ## 🙏 Acknowledgments
325
+
326
+ - **Google Gemini AI** - For providing the AI capabilities
327
+ - **Spring Team** - For the excellent Spring Boot framework
328
+ - **Contributors** - Thanks to all who contribute to this project
329
+
330
+ ## 📞 Support
331
+
332
+ - 📧 **Email**: [email protected]
333
+ - 🐛 **Issues**: [GitHub Issues](https://github.com/aniketqw/spring-gemini-chat/issues)
334
+ - 💬 **Discussions**: [GitHub Discussions](https://github.com/aniketqw/spring-gemini-chat/discussions)
335
+
336
+ ---
337
+
pom.xml ADDED
@@ -0,0 +1,167 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ https://maven.apache.org/xsd/maven-4.0.0.xsd">
6
+ <modelVersion>4.0.0</modelVersion>
7
+
8
+ <parent>
9
+ <groupId>org.springframework.boot</groupId>
10
+ <artifactId>spring-boot-starter-parent</artifactId>
11
+ <version>3.1.5</version>
12
+ <relativePath/>
13
+ </parent>
14
+
15
+ <groupId>com.example</groupId>
16
+ <artifactId>p1</artifactId>
17
+ <version>0.0.1-SNAPSHOT</version>
18
+ <name>p1</name>
19
+ <description>Chat application with Gemini AI</description>
20
+
21
+ <properties>
22
+ <java.version>17</java.version>
23
+ <maven.compiler.source>17</maven.compiler.source>
24
+ <maven.compiler.target>17</maven.compiler.target>
25
+ <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
26
+ </properties>
27
+
28
+ <dependencies>
29
+ <!-- Spring Boot Web Starter -->
30
+ <dependency>
31
+ <groupId>org.springframework.boot</groupId>
32
+ <artifactId>spring-boot-starter-web</artifactId>
33
+ </dependency>
34
+
35
+ <!-- Spring Boot WebFlux for reactive programming -->
36
+ <dependency>
37
+ <groupId>org.springframework.boot</groupId>
38
+ <artifactId>spring-boot-starter-webflux</artifactId>
39
+ </dependency>
40
+
41
+ <!-- Spring Boot Validation -->
42
+ <dependency>
43
+ <groupId>org.springframework.boot</groupId>
44
+ <artifactId>spring-boot-starter-validation</artifactId>
45
+ </dependency>
46
+
47
+ <!-- Configuration Processor -->
48
+ <dependency>
49
+ <groupId>org.springframework.boot</groupId>
50
+ <artifactId>spring-boot-configuration-processor</artifactId>
51
+ <optional>true</optional>
52
+ </dependency>
53
+
54
+ <!-- Jackson for JSON processing -->
55
+ <dependency>
56
+ <groupId>com.fasterxml.jackson.core</groupId>
57
+ <artifactId>jackson-databind</artifactId>
58
+ </dependency>
59
+
60
+ <!-- Spring Boot Test Starter -->
61
+ <dependency>
62
+ <groupId>org.springframework.boot</groupId>
63
+ <artifactId>spring-boot-starter-test</artifactId>
64
+ <scope>test</scope>
65
+ </dependency>
66
+
67
+ <!-- Reactor Test for reactive testing -->
68
+ <dependency>
69
+ <groupId>io.projectreactor</groupId>
70
+ <artifactId>reactor-test</artifactId>
71
+ <scope>test</scope>
72
+ </dependency>
73
+ <dependency>
74
+ <groupId>org.mockito</groupId>
75
+ <artifactId>mockito-core</artifactId>
76
+ <version>5.3.1</version>
77
+ <scope>compile</scope>
78
+ </dependency>
79
+ <dependency>
80
+ <groupId>org.testng</groupId>
81
+ <artifactId>testng</artifactId>
82
+ <version>RELEASE</version>
83
+ <scope>compile</scope>
84
+ </dependency>
85
+ <dependency>
86
+ <groupId>org.springframework.boot</groupId>
87
+ <artifactId>spring-boot-test-autoconfigure</artifactId>
88
+ <version>3.1.5</version>
89
+ <scope>compile</scope>
90
+ </dependency>
91
+ <dependency>
92
+ <groupId>org.springframework</groupId>
93
+ <artifactId>spring-test</artifactId>
94
+ <version>6.0.13</version>
95
+ <scope>compile</scope>
96
+ </dependency>
97
+ <dependency>
98
+ <groupId>org.springframework.boot</groupId>
99
+ <artifactId>spring-boot-starter-actuator</artifactId>
100
+ </dependency>
101
+ </dependencies>
102
+
103
+ <build>
104
+ <plugins>
105
+ <plugin>
106
+ <groupId>org.springframework.boot</groupId>
107
+ <artifactId>spring-boot-maven-plugin</artifactId>
108
+ <configuration>
109
+ <excludes>
110
+ <exclude>
111
+ <groupId>org.springframework.boot</groupId>
112
+ <artifactId>spring-boot-configuration-processor</artifactId>
113
+ </exclude>
114
+ </excludes>
115
+ </configuration>
116
+ </plugin>
117
+
118
+ <plugin>
119
+ <groupId>org.apache.maven.plugins</groupId>
120
+ <artifactId>maven-compiler-plugin</artifactId>
121
+ <version>3.11.0</version>
122
+ <configuration>
123
+ <source>17</source>
124
+ <target>17</target>
125
+ </configuration>
126
+ </plugin>
127
+ </plugins>
128
+ </build>
129
+
130
+ <repositories>
131
+ <repository>
132
+ <id>spring-milestones</id>
133
+ <name>Spring Milestones</name>
134
+ <url>https://repo.spring.io/milestone</url>
135
+ <snapshots>
136
+ <enabled>false</enabled>
137
+ </snapshots>
138
+ </repository>
139
+ <repository>
140
+ <id>spring-snapshots</id>
141
+ <name>Spring Snapshots</name>
142
+ <url>https://repo.spring.io/snapshot</url>
143
+ <releases>
144
+ <enabled>false</enabled>
145
+ </releases>
146
+ </repository>
147
+ </repositories>
148
+
149
+ <pluginRepositories>
150
+ <pluginRepository>
151
+ <id>spring-milestones</id>
152
+ <name>Spring Milestones</name>
153
+ <url>https://repo.spring.io/milestone</url>
154
+ <snapshots>
155
+ <enabled>false</enabled>
156
+ </snapshots>
157
+ </pluginRepository>
158
+ <pluginRepository>
159
+ <id>spring-snapshots</id>
160
+ <name>Spring Snapshots</name>
161
+ <url>https://repo.spring.io/snapshot</url>
162
+ <releases>
163
+ <enabled>false</enabled>
164
+ </releases>
165
+ </pluginRepository>
166
+ </pluginRepositories>
167
+ </project>
screenshots/chat-interface.png ADDED

Git LFS Details

  • SHA256: aaa7f4ddc0802ab62f5934981ffdaa10cc2cd8c10b3aa22a2223ebc81fe5cd42
  • Pointer size: 131 Bytes
  • Size of remote file: 316 kB
screenshots/health-check.png ADDED

Git LFS Details

  • SHA256: 978ba61ab11ea61f286122ca074af49c37048e19356a0ed7091352a4e56ac571
  • Pointer size: 130 Bytes
  • Size of remote file: 10.8 kB
src/index.html ADDED
@@ -0,0 +1,499 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Gemini Chat - Test UI</title>
7
+ <style>
8
+ * {
9
+ margin: 0;
10
+ padding: 0;
11
+ box-sizing: border-box;
12
+ }
13
+
14
+ body {
15
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
16
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
17
+ height: 100vh;
18
+ display: flex;
19
+ justify-content: center;
20
+ align-items: center;
21
+ }
22
+
23
+ .chat-container {
24
+ background: white;
25
+ border-radius: 20px;
26
+ box-shadow: 0 20px 40px rgba(0,0,0,0.1);
27
+ width: 90%;
28
+ max-width: 800px;
29
+ height: 600px;
30
+ display: flex;
31
+ flex-direction: column;
32
+ overflow: hidden;
33
+ }
34
+
35
+ .chat-header {
36
+ background: linear-gradient(45deg, #4CAF50, #45a049);
37
+ color: white;
38
+ padding: 20px;
39
+ text-align: center;
40
+ display: flex;
41
+ justify-content: space-between;
42
+ align-items: center;
43
+ }
44
+
45
+ .header-info {
46
+ display: flex;
47
+ flex-direction: column;
48
+ align-items: flex-start;
49
+ }
50
+
51
+ .status-indicator {
52
+ display: flex;
53
+ align-items: center;
54
+ gap: 8px;
55
+ font-size: 0.9em;
56
+ opacity: 0.9;
57
+ }
58
+
59
+ .status-dot {
60
+ width: 8px;
61
+ height: 8px;
62
+ border-radius: 50%;
63
+ background: #ff4444;
64
+ animation: pulse 2s infinite;
65
+ }
66
+
67
+ .status-dot.online {
68
+ background: #44ff44;
69
+ }
70
+
71
+ @keyframes pulse {
72
+ 0% { opacity: 1; }
73
+ 50% { opacity: 0.5; }
74
+ 100% { opacity: 1; }
75
+ }
76
+
77
+ .chat-messages {
78
+ flex: 1;
79
+ padding: 20px;
80
+ overflow-y: auto;
81
+ background: #f8f9fa;
82
+ display: flex;
83
+ flex-direction: column;
84
+ gap: 15px;
85
+ }
86
+
87
+ .message {
88
+ display: flex;
89
+ gap: 10px;
90
+ animation: slideIn 0.3s ease-out;
91
+ }
92
+
93
+ @keyframes slideIn {
94
+ from {
95
+ opacity: 0;
96
+ transform: translateY(20px);
97
+ }
98
+ to {
99
+ opacity: 1;
100
+ transform: translateY(0);
101
+ }
102
+ }
103
+
104
+ .message.user {
105
+ justify-content: flex-end;
106
+ }
107
+
108
+ .message-bubble {
109
+ max-width: 70%;
110
+ padding: 12px 18px;
111
+ border-radius: 18px;
112
+ word-wrap: break-word;
113
+ line-height: 1.4;
114
+ }
115
+
116
+ .message.user .message-bubble {
117
+ background: linear-gradient(45deg, #007bff, #0056b3);
118
+ color: white;
119
+ border-bottom-right-radius: 4px;
120
+ }
121
+
122
+ .message.bot .message-bubble {
123
+ background: white;
124
+ border: 1px solid #e0e0e0;
125
+ color: #333;
126
+ border-bottom-left-radius: 4px;
127
+ box-shadow: 0 2px 5px rgba(0,0,0,0.1);
128
+ }
129
+
130
+ .message-time {
131
+ font-size: 0.7em;
132
+ opacity: 0.7;
133
+ margin-top: 4px;
134
+ }
135
+
136
+ .typing-indicator {
137
+ display: none;
138
+ align-items: center;
139
+ gap: 10px;
140
+ padding: 15px 20px;
141
+ font-style: italic;
142
+ color: #666;
143
+ }
144
+
145
+ .typing-dots {
146
+ display: flex;
147
+ gap: 4px;
148
+ }
149
+
150
+ .typing-dot {
151
+ width: 8px;
152
+ height: 8px;
153
+ border-radius: 50%;
154
+ background: #4CAF50;
155
+ animation: typingDot 1.4s infinite ease-in-out;
156
+ }
157
+
158
+ .typing-dot:nth-child(2) { animation-delay: 0.2s; }
159
+ .typing-dot:nth-child(3) { animation-delay: 0.4s; }
160
+
161
+ @keyframes typingDot {
162
+ 0%, 60%, 100% { transform: translateY(0); }
163
+ 30% { transform: translateY(-10px); }
164
+ }
165
+
166
+ .chat-input-container {
167
+ padding: 20px;
168
+ background: white;
169
+ border-top: 1px solid #e0e0e0;
170
+ }
171
+
172
+ .chat-input-form {
173
+ display: flex;
174
+ gap: 10px;
175
+ align-items: center;
176
+ }
177
+
178
+ .chat-input {
179
+ flex: 1;
180
+ padding: 12px 18px;
181
+ border: 2px solid #e0e0e0;
182
+ border-radius: 25px;
183
+ outline: none;
184
+ font-size: 16px;
185
+ transition: border-color 0.3s;
186
+ }
187
+
188
+ .chat-input:focus {
189
+ border-color: #4CAF50;
190
+ }
191
+
192
+ .send-button {
193
+ background: linear-gradient(45deg, #4CAF50, #45a049);
194
+ color: white;
195
+ border: none;
196
+ border-radius: 50%;
197
+ width: 50px;
198
+ height: 50px;
199
+ cursor: pointer;
200
+ display: flex;
201
+ align-items: center;
202
+ justify-content: center;
203
+ transition: transform 0.2s, box-shadow 0.2s;
204
+ font-size: 20px;
205
+ }
206
+
207
+ .send-button:hover {
208
+ transform: scale(1.05);
209
+ box-shadow: 0 5px 15px rgba(76, 175, 80, 0.4);
210
+ }
211
+
212
+ .send-button:disabled {
213
+ background: #ccc;
214
+ cursor: not-allowed;
215
+ transform: none;
216
+ box-shadow: none;
217
+ }
218
+
219
+ .error-message {
220
+ background: #ffebee;
221
+ color: #c62828;
222
+ padding: 10px;
223
+ border-radius: 5px;
224
+ margin: 10px 20px;
225
+ border-left: 4px solid #f44336;
226
+ display: none;
227
+ }
228
+
229
+ .health-status {
230
+ font-size: 0.8em;
231
+ opacity: 0.8;
232
+ }
233
+
234
+ /* Responsive Design */
235
+ @media (max-width: 600px) {
236
+ .chat-container {
237
+ width: 95%;
238
+ height: 90vh;
239
+ margin: 20px;
240
+ }
241
+
242
+ .chat-header {
243
+ padding: 15px;
244
+ }
245
+
246
+ .chat-messages {
247
+ padding: 15px;
248
+ }
249
+
250
+ .message-bubble {
251
+ max-width: 85%;
252
+ }
253
+
254
+ .chat-input-container {
255
+ padding: 15px;
256
+ }
257
+ }
258
+
259
+ /* Scrollbar Styling */
260
+ .chat-messages::-webkit-scrollbar {
261
+ width: 6px;
262
+ }
263
+
264
+ .chat-messages::-webkit-scrollbar-track {
265
+ background: #f1f1f1;
266
+ border-radius: 3px;
267
+ }
268
+
269
+ .chat-messages::-webkit-scrollbar-thumb {
270
+ background: #c1c1c1;
271
+ border-radius: 3px;
272
+ }
273
+
274
+ .chat-messages::-webkit-scrollbar-thumb:hover {
275
+ background: #a8a8a8;
276
+ }
277
+ </style>
278
+ </head>
279
+ <body>
280
+ <div class="chat-container">
281
+ <div class="chat-header">
282
+ <div class="header-info">
283
+ <h2>🤖 Gemini Chat Assistant</h2>
284
+ <div class="status-indicator">
285
+ <div class="status-dot" id="statusDot"></div>
286
+ <span id="statusText">Checking connection...</span>
287
+ </div>
288
+ </div>
289
+ <div class="health-status">
290
+ <button onclick="checkHealth()" style="background: rgba(255,255,255,0.2); border: 1px solid rgba(255,255,255,0.3); color: white; padding: 5px 10px; border-radius: 15px; cursor: pointer; font-size: 0.8em;">
291
+ 🔄 Check Status
292
+ </button>
293
+ </div>
294
+ </div>
295
+
296
+ <div class="error-message" id="errorMessage"></div>
297
+
298
+ <div class="chat-messages" id="chatMessages">
299
+ <div class="message bot">
300
+ <div class="message-bubble">
301
+ 👋 Hello! I'm your Gemini-powered chat assistant. How can I help you today?
302
+ <div class="message-time" id="welcomeTime"></div>
303
+ </div>
304
+ </div>
305
+ </div>
306
+
307
+ <div class="typing-indicator" id="typingIndicator">
308
+ <span>Gemini is thinking</span>
309
+ <div class="typing-dots">
310
+ <div class="typing-dot"></div>
311
+ <div class="typing-dot"></div>
312
+ <div class="typing-dot"></div>
313
+ </div>
314
+ </div>
315
+
316
+ <div class="chat-input-container">
317
+ <form class="chat-input-form" id="chatForm">
318
+ <input
319
+ type="text"
320
+ class="chat-input"
321
+ id="messageInput"
322
+ placeholder="Type your message here..."
323
+ autocomplete="off"
324
+ maxlength="1000"
325
+ >
326
+ <button type="submit" class="send-button" id="sendButton">
327
+
328
+ </button>
329
+ </form>
330
+ </div>
331
+ </div>
332
+
333
+ <script>
334
+ // Configuration
335
+ const API_BASE_URL = 'http://localhost:8080/api/chat';
336
+
337
+ // DOM Elements
338
+ const chatMessages = document.getElementById('chatMessages');
339
+ const messageInput = document.getElementById('messageInput');
340
+ const sendButton = document.getElementById('sendButton');
341
+ const chatForm = document.getElementById('chatForm');
342
+ const typingIndicator = document.getElementById('typingIndicator');
343
+ const errorMessage = document.getElementById('errorMessage');
344
+ const statusDot = document.getElementById('statusDot');
345
+ const statusText = document.getElementById('statusText');
346
+
347
+ // Initialize
348
+ document.addEventListener('DOMContentLoaded', function() {
349
+ document.getElementById('welcomeTime').textContent = getCurrentTime();
350
+ checkHealth();
351
+ messageInput.focus();
352
+ });
353
+
354
+ // Event Listeners
355
+ chatForm.addEventListener('submit', function(e) {
356
+ e.preventDefault();
357
+ sendMessage();
358
+ });
359
+
360
+ messageInput.addEventListener('keypress', function(e) {
361
+ if (e.key === 'Enter' && !e.shiftKey) {
362
+ e.preventDefault();
363
+ sendMessage();
364
+ }
365
+ });
366
+
367
+ // Functions
368
+ function getCurrentTime() {
369
+ return new Date().toLocaleTimeString('en-US', {
370
+ hour12: true,
371
+ hour: 'numeric',
372
+ minute: '2-digit'
373
+ });
374
+ }
375
+
376
+ function showError(message) {
377
+ errorMessage.textContent = message;
378
+ errorMessage.style.display = 'block';
379
+ setTimeout(() => {
380
+ errorMessage.style.display = 'none';
381
+ }, 5000);
382
+ }
383
+
384
+ function updateStatus(online, message) {
385
+ statusDot.className = `status-dot ${online ? 'online' : ''}`;
386
+ statusText.textContent = message;
387
+ }
388
+
389
+ async function checkHealth() {
390
+ try {
391
+ const response = await fetch(`${API_BASE_URL}/health`);
392
+ if (response.ok) {
393
+ const data = await response.json();
394
+ updateStatus(true, `Connected • ${data.service}`);
395
+ } else {
396
+ updateStatus(false, 'Backend unavailable');
397
+ }
398
+ } catch (error) {
399
+ updateStatus(false, 'Connection failed');
400
+ console.error('Health check failed:', error);
401
+ }
402
+ }
403
+
404
+ function addMessage(content, isUser = false) {
405
+ const messageDiv = document.createElement('div');
406
+ messageDiv.className = `message ${isUser ? 'user' : 'bot'}`;
407
+
408
+ const bubbleDiv = document.createElement('div');
409
+ bubbleDiv.className = 'message-bubble';
410
+ bubbleDiv.innerHTML = `
411
+ ${content}
412
+ <div class="message-time">${getCurrentTime()}</div>
413
+ `;
414
+
415
+ messageDiv.appendChild(bubbleDiv);
416
+ chatMessages.appendChild(messageDiv);
417
+
418
+ // Scroll to bottom
419
+ chatMessages.scrollTop = chatMessages.scrollHeight;
420
+ }
421
+
422
+ function showTypingIndicator() {
423
+ typingIndicator.style.display = 'flex';
424
+ chatMessages.scrollTop = chatMessages.scrollHeight;
425
+ }
426
+
427
+ function hideTypingIndicator() {
428
+ typingIndicator.style.display = 'none';
429
+ }
430
+
431
+ function setLoading(loading) {
432
+ sendButton.disabled = loading;
433
+ sendButton.textContent = loading ? '⏳' : '➤';
434
+ messageInput.disabled = loading;
435
+
436
+ if (loading) {
437
+ showTypingIndicator();
438
+ } else {
439
+ hideTypingIndicator();
440
+ }
441
+ }
442
+
443
+ async function sendMessage() {
444
+ const message = messageInput.value.trim();
445
+ if (!message) return;
446
+
447
+ // Add user message
448
+ addMessage(message, true);
449
+ messageInput.value = '';
450
+ setLoading(true);
451
+
452
+ try {
453
+ const response = await fetch(`${API_BASE_URL}/message`, {
454
+ method: 'POST',
455
+ headers: {
456
+ 'Content-Type': 'application/json',
457
+ },
458
+ body: JSON.stringify({ message: message })
459
+ });
460
+
461
+ if (!response.ok) {
462
+ throw new Error(`HTTP error! status: ${response.status}`);
463
+ }
464
+
465
+ const data = await response.json();
466
+
467
+ if (data.success) {
468
+ // Format the response for better display
469
+ const formattedResponse = data.response
470
+ .replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
471
+ .replace(/\n/g, '<br>');
472
+
473
+ addMessage(formattedResponse, false);
474
+ } else {
475
+ throw new Error(data.error || 'Unknown error occurred');
476
+ }
477
+
478
+ } catch (error) {
479
+ console.error('Error sending message:', error);
480
+ showError(`Failed to send message: ${error.message}`);
481
+ addMessage('😔 Sorry, I encountered an error. Please try again or check if the backend is running.', false);
482
+ } finally {
483
+ setLoading(false);
484
+ messageInput.focus();
485
+ }
486
+ }
487
+
488
+ // Auto-focus input when clicking anywhere in chat area
489
+ chatMessages.addEventListener('click', () => {
490
+ if (!messageInput.disabled) {
491
+ messageInput.focus();
492
+ }
493
+ });
494
+
495
+ // Check health status periodically
496
+ setInterval(checkHealth, 30000); // Check every 30 seconds
497
+ </script>
498
+ </body>
499
+ </html>
src/main/java/com/example/p1/P1Application.java ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.example.p1;
2
+
3
+
4
+ import org.springframework.boot.SpringApplication;
5
+ import org.springframework.boot.autoconfigure.SpringBootApplication;
6
+
7
+ @SpringBootApplication
8
+ public class P1Application {
9
+ public static void main(String[] args) {
10
+ SpringApplication.run(P1Application.class, args);
11
+ }
12
+ }
src/main/java/com/example/p1/config/AiConfiguration.java ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.example.p1.config;
2
+
3
+ import org.springframework.boot.context.properties.ConfigurationProperties;
4
+ import org.springframework.context.annotation.Bean;
5
+ import org.springframework.context.annotation.Configuration;
6
+ import org.springframework.web.reactive.function.client.WebClient;
7
+ import org.springframework.web.servlet.config.annotation.CorsRegistry;
8
+ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
9
+
10
+ @Configuration
11
+ @ConfigurationProperties(prefix = "gemini.api")
12
+ public class AiConfiguration implements WebMvcConfigurer {
13
+
14
+ private String key;
15
+ private String baseUrl;
16
+ private String model;
17
+ private int timeout = 30000;
18
+
19
+ @Bean
20
+ public WebClient geminiWebClient() {
21
+ return WebClient.builder()
22
+ .baseUrl(baseUrl)
23
+ .defaultHeader("Content-Type", "application/json")
24
+ .codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(1024 * 1024))
25
+ .build();
26
+ }
27
+
28
+ @Override
29
+ public void addCorsMappings(CorsRegistry registry) {
30
+ registry.addMapping("/api/**")
31
+ .allowedOrigins("http://localhost:3000", "http://localhost:8080","http://localhost:63342")
32
+ .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
33
+ .allowedHeaders("*")
34
+ .allowCredentials(true);
35
+ }
36
+
37
+ // Getters and setters
38
+ public String getKey() { return key; }
39
+ public void setKey(String key) { this.key = key; }
40
+
41
+ public String getBaseUrl() { return baseUrl; }
42
+ public void setBaseUrl(String baseUrl) { this.baseUrl = baseUrl; }
43
+
44
+ public String getModel() { return model; }
45
+ public void setModel(String model) { this.model = model; }
46
+
47
+ public int getTimeout() { return timeout; }
48
+ public void setTimeout(int timeout) { this.timeout = timeout; }
49
+ }
src/main/java/com/example/p1/controller/ChatController.java ADDED
@@ -0,0 +1,130 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.example.p1.controller;
2
+
3
+ import com.example.p1.service.ChatService;
4
+ import org.springframework.http.MediaType;
5
+ import org.springframework.http.ResponseEntity;
6
+ import org.springframework.web.bind.annotation.*;
7
+ import reactor.core.publisher.Flux;
8
+ import reactor.core.publisher.Mono;
9
+
10
+ import jakarta.validation.Valid;
11
+ import jakarta.validation.constraints.NotBlank;
12
+ import java.util.List;
13
+ import java.util.Map;
14
+
15
+ @RestController
16
+ @RequestMapping("/api/chat")
17
+ @CrossOrigin(origins = {"http://localhost:3000", "http://localhost:8080"})
18
+ public class ChatController {
19
+
20
+ private final ChatService chatService;
21
+
22
+ public ChatController(ChatService chatService) {
23
+ this.chatService = chatService;
24
+ }
25
+
26
+ @PostMapping("/message")
27
+ public Mono<ResponseEntity<ChatResponse>> sendMessage(@Valid @RequestBody ChatRequest request) {
28
+ return chatService.generateResponse(request.getMessage())
29
+ .map(response -> ResponseEntity.ok(new ChatResponse(response, true, null)))
30
+ .onErrorReturn(ResponseEntity.badRequest()
31
+ .body(new ChatResponse(null, false, "Failed to generate response")));
32
+ }
33
+
34
+ @PostMapping("/conversation")
35
+ public Mono<ResponseEntity<ChatResponse>> sendConversation(@Valid @RequestBody ConversationRequest request) {
36
+ return chatService.generateConversationResponse(request.getMessages())
37
+ .map(response -> ResponseEntity.ok(new ChatResponse(response, true, null)))
38
+ .onErrorReturn(ResponseEntity.badRequest()
39
+ .body(new ChatResponse(null, false, "Failed to generate response")));
40
+ }
41
+
42
+ @PostMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
43
+ public Flux<String> streamMessage(@Valid @RequestBody ChatRequest request) {
44
+ return chatService.generateStreamingResponse(request.getMessage());
45
+ }
46
+
47
+ @GetMapping("/health")
48
+ public ResponseEntity<Map<String, Object>> health() {
49
+ return ResponseEntity.ok(Map.of(
50
+ "status", "UP",
51
+ "service", "Chat Service",
52
+ "timestamp", System.currentTimeMillis()
53
+ ));
54
+ }
55
+
56
+ // DTOs
57
+ public static class ChatRequest {
58
+ @NotBlank(message = "Message cannot be blank")
59
+ private String message;
60
+
61
+ public ChatRequest() {}
62
+
63
+ public ChatRequest(String message) {
64
+ this.message = message;
65
+ }
66
+
67
+ public String getMessage() { return message; }
68
+ public void setMessage(String message) { this.message = message; }
69
+ }
70
+
71
+ public static class ConversationRequest {
72
+ @Valid
73
+ private List<Message> messages;
74
+
75
+ public ConversationRequest() {}
76
+
77
+ public ConversationRequest(List<Message> messages) {
78
+ this.messages = messages;
79
+ }
80
+
81
+ public List<Message> getMessages() { return messages; }
82
+ public void setMessages(List<Message> messages) { this.messages = messages; }
83
+
84
+ public static class Message {
85
+ @NotBlank
86
+ private String role; // "user" or "assistant"
87
+ @NotBlank
88
+ private String content;
89
+
90
+ public Message() {}
91
+
92
+ public Message(String role, String content) {
93
+ this.role = role;
94
+ this.content = content;
95
+ }
96
+
97
+ public String getRole() { return role; }
98
+ public void setRole(String role) { this.role = role; }
99
+
100
+ public String getContent() { return content; }
101
+ public void setContent(String content) { this.content = content; }
102
+ }
103
+ }
104
+
105
+ public static class ChatResponse {
106
+ private String response;
107
+ private boolean success;
108
+ private String error;
109
+ private long timestamp;
110
+
111
+ public ChatResponse(String response, boolean success, String error) {
112
+ this.response = response;
113
+ this.success = success;
114
+ this.error = error;
115
+ this.timestamp = System.currentTimeMillis();
116
+ }
117
+
118
+ public String getResponse() { return response; }
119
+ public void setResponse(String response) { this.response = response; }
120
+
121
+ public boolean isSuccess() { return success; }
122
+ public void setSuccess(boolean success) { this.success = success; }
123
+
124
+ public String getError() { return error; }
125
+ public void setError(String error) { this.error = error; }
126
+
127
+ public long getTimestamp() { return timestamp; }
128
+ public void setTimestamp(long timestamp) { this.timestamp = timestamp; }
129
+ }
130
+ }
src/main/java/com/example/p1/service/ChatService.java ADDED
@@ -0,0 +1,166 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.example.p1.service;
2
+
3
+ import com.example.p1.config.AiConfiguration;
4
+ import com.example.p1.controller.ChatController;
5
+ import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
6
+ import com.fasterxml.jackson.annotation.JsonProperty;
7
+ import org.slf4j.Logger;
8
+ import org.slf4j.LoggerFactory;
9
+ import org.springframework.stereotype.Service;
10
+ import org.springframework.web.reactive.function.client.WebClient;
11
+ import reactor.core.publisher.Flux;
12
+ import reactor.core.publisher.Mono;
13
+
14
+ import java.time.Duration;
15
+ import java.util.List;
16
+ import java.util.stream.Collectors;
17
+
18
+ @Service
19
+ public class ChatService {
20
+ private static final Logger logger = LoggerFactory.getLogger(ChatService.class);
21
+
22
+ private final WebClient webClient;
23
+ private final AiConfiguration config;
24
+
25
+ public ChatService(WebClient geminiWebClient, AiConfiguration config) {
26
+ this.webClient = geminiWebClient;
27
+ this.config = config;
28
+ }
29
+
30
+ public Mono<String> generateResponse(String message) {
31
+ logger.debug("Generating response for message: {}", message);
32
+
33
+ GeminiRequest request = createSimpleRequest(message);
34
+
35
+ return callGeminiAPI(request)
36
+ .map(this::extractTextFromResponse)
37
+ .doOnSuccess(response -> logger.debug("Generated response: {}", response))
38
+ .doOnError(error -> logger.error("Error generating response", error));
39
+ }
40
+
41
+ public Mono<String> generateConversationResponse(List<ChatController.ConversationRequest.Message> messages) {
42
+ logger.debug("Generating conversation response for {} messages", messages.size());
43
+
44
+ String conversationContext = buildConversationContext(messages);
45
+ return generateResponse(conversationContext);
46
+ }
47
+
48
+ public Flux<String> generateStreamingResponse(String message) {
49
+ // For demonstration - in real implementation, you'd use Gemini's streaming API
50
+ return generateResponse(message)
51
+ .flatMapMany(response -> {
52
+ String[] words = response.split("\\s+");
53
+ return Flux.fromArray(words)
54
+ .delayElements(Duration.ofMillis(100))
55
+ .map(word -> word + " ");
56
+ });
57
+ }
58
+
59
+ private Mono<GeminiResponse> callGeminiAPI(GeminiRequest request) {
60
+ return webClient.post()
61
+ .uri("/{model}:generateContent?key={apiKey}",
62
+ config.getModel(), config.getKey())
63
+ .bodyValue(request)
64
+ .retrieve()
65
+ .bodyToMono(GeminiResponse.class)
66
+ .timeout(Duration.ofMillis(config.getTimeout()));
67
+ }
68
+
69
+ private GeminiRequest createSimpleRequest(String message) {
70
+ GeminiRequest.Part part = new GeminiRequest.Part(message);
71
+ GeminiRequest.Content content = new GeminiRequest.Content(List.of(part));
72
+ return new GeminiRequest(List.of(content));
73
+ }
74
+
75
+ private String buildConversationContext(List<ChatController.ConversationRequest.Message> messages) {
76
+ StringBuilder context = new StringBuilder();
77
+ context.append("Previous conversation:\n");
78
+
79
+ for (ChatController.ConversationRequest.Message msg : messages) {
80
+ context.append(msg.getRole().toUpperCase())
81
+ .append(": ")
82
+ .append(msg.getContent())
83
+ .append("\n");
84
+ }
85
+
86
+ return context.toString();
87
+ }
88
+
89
+ private String extractTextFromResponse(GeminiResponse response) {
90
+ if (response.getCandidates() != null && !response.getCandidates().isEmpty()) {
91
+ GeminiResponse.Candidate candidate = response.getCandidates().get(0);
92
+ if (candidate.getContent() != null &&
93
+ candidate.getContent().getParts() != null &&
94
+ !candidate.getContent().getParts().isEmpty()) {
95
+ return candidate.getContent().getParts().get(0).getText();
96
+ }
97
+ }
98
+ return "No response generated";
99
+ }
100
+
101
+ // Gemini API DTOs
102
+ public static class GeminiRequest {
103
+ private List<Content> contents;
104
+
105
+ public GeminiRequest(List<Content> contents) {
106
+ this.contents = contents;
107
+ }
108
+
109
+ public List<Content> getContents() { return contents; }
110
+ public void setContents(List<Content> contents) { this.contents = contents; }
111
+
112
+ public static class Content {
113
+ private List<Part> parts;
114
+
115
+ public Content(List<Part> parts) {
116
+ this.parts = parts;
117
+ }
118
+
119
+ public List<Part> getParts() { return parts; }
120
+ public void setParts(List<Part> parts) { this.parts = parts; }
121
+ }
122
+
123
+ public static class Part {
124
+ private String text;
125
+
126
+ public Part(String text) {
127
+ this.text = text;
128
+ }
129
+
130
+ public String getText() { return text; }
131
+ public void setText(String text) { this.text = text; }
132
+ }
133
+ }
134
+
135
+ @JsonIgnoreProperties(ignoreUnknown = true)
136
+ public static class GeminiResponse {
137
+ private List<Candidate> candidates;
138
+
139
+ public List<Candidate> getCandidates() { return candidates; }
140
+ public void setCandidates(List<Candidate> candidates) { this.candidates = candidates; }
141
+
142
+ @JsonIgnoreProperties(ignoreUnknown = true)
143
+ public static class Candidate {
144
+ private Content content;
145
+
146
+ public Content getContent() { return content; }
147
+ public void setContent(Content content) { this.content = content; }
148
+ }
149
+
150
+ @JsonIgnoreProperties(ignoreUnknown = true)
151
+ public static class Content {
152
+ private List<Part> parts;
153
+
154
+ public List<Part> getParts() { return parts; }
155
+ public void setParts(List<Part> parts) { this.parts = parts; }
156
+ }
157
+
158
+ @JsonIgnoreProperties(ignoreUnknown = true)
159
+ public static class Part {
160
+ private String text;
161
+
162
+ public String getText() { return text; }
163
+ public void setText(String text) { this.text = text; }
164
+ }
165
+ }
166
+ }
src/main/resources/application.properties.txt ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ spring.application.name=p1
2
+
3
+ # Server Configuration
4
+ server.port=8080
5
+
6
+ # Gemini AI Configuration
7
+ gemini.api.key=${GEMINI_API_KEY:YOUR_KEY}
8
+ gemini.api.base-url=https://generativelanguage.googleapis.com/v1beta/models
9
+ gemini.api.model=gemini-2.5-flash
10
+ gemini.api.timeout=30000
11
+
12
+ # Logging
13
+ logging.level.com.example.p1=DEBUG
14
+ logging.level.org.springframework.web=DEBUG
15
+
16
+ # CORS Configuration
17
+ cors.allowed-origins=http://localhost:3000,http://localhost:8080
18
+ cors.allowed-methods=GET,POST,PUT,DELETE,OPTIONS
19
+ cors.allowed-headers=*
20
+ */
src/test/java/com/example/p1/P1ApplicationTests.java ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.example.p1;
2
+
3
+ import org.junit.jupiter.api.Test;
4
+ import org.springframework.boot.test.context.SpringBootTest;
5
+
6
+ @SpringBootTest
7
+ class P1ApplicationTests {
8
+
9
+ @Test
10
+ void contextLoads() {
11
+ }
12
+
13
+ }