leedstack 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +364 -0
- package/bin/create-stack.js +277 -0
- package/package.json +60 -0
- package/tools/templates/backend/go-echo/backend/.env.example +10 -0
- package/tools/templates/backend/go-echo/backend/cmd/server/main.go.ejs +57 -0
- package/tools/templates/backend/go-echo/backend/go.mod.ejs +10 -0
- package/tools/templates/backend/go-echo/backend/internal/handlers/example.go +15 -0
- package/tools/templates/backend/go-echo/backend/internal/handlers/health.go +13 -0
- package/tools/templates/backend/java-spring/backend/.env.example +10 -0
- package/tools/templates/backend/java-spring/backend/pom.xml.ejs +64 -0
- package/tools/templates/backend/java-spring/backend/src/main/java/com/app/Application.java.ejs +11 -0
- package/tools/templates/backend/java-spring/backend/src/main/java/com/app/config/SecurityConfig.java.ejs +64 -0
- package/tools/templates/backend/java-spring/backend/src/main/java/com/app/controller/ExampleController.java +19 -0
- package/tools/templates/backend/java-spring/backend/src/main/java/com/app/controller/HealthController.java +15 -0
- package/tools/templates/backend/java-spring/backend/src/main/resources/application.yml.ejs +20 -0
- package/tools/templates/backend/node-express/backend/.env.example +10 -0
- package/tools/templates/backend/node-express/backend/.eslintrc.json +21 -0
- package/tools/templates/backend/node-express/backend/.prettierrc +10 -0
- package/tools/templates/backend/node-express/backend/Dockerfile +52 -0
- package/tools/templates/backend/node-express/backend/README.md +68 -0
- package/tools/templates/backend/node-express/backend/package.json.ejs +37 -0
- package/tools/templates/backend/node-express/backend/src/index.ts.ejs +75 -0
- package/tools/templates/backend/node-express/backend/src/routes/health.ts +54 -0
- package/tools/templates/backend/node-express/backend/tsconfig.json +17 -0
- package/tools/templates/backend/python-fastapi/backend/.env.example +18 -0
- package/tools/templates/backend/python-fastapi/backend/README.md +73 -0
- package/tools/templates/backend/python-fastapi/backend/app/__init__.py +1 -0
- package/tools/templates/backend/python-fastapi/backend/app/main.py.ejs +68 -0
- package/tools/templates/backend/python-fastapi/backend/requirements.txt.ejs +22 -0
- package/tools/templates/base/.dockerignore +16 -0
- package/tools/templates/base/.env.example +31 -0
- package/tools/templates/base/.github/workflows/ci.yml.ejs +124 -0
- package/tools/templates/base/.github/workflows/deploy-separate.yml.example +144 -0
- package/tools/templates/base/.vscode/extensions.json +17 -0
- package/tools/templates/base/.vscode/settings.json +49 -0
- package/tools/templates/base/Makefile +98 -0
- package/tools/templates/base/README.md.ejs +118 -0
- package/tools/templates/base/docker-compose.yml.ejs +49 -0
- package/tools/templates/base/package.json.ejs +30 -0
- package/tools/templates/base/scripts/split-repos.sh +189 -0
- package/tools/templates/db/postgres/backend/java-spring/backend/pom.xml.ejs +81 -0
- package/tools/templates/db/postgres/backend/java-spring/backend/src/main/resources/application.yml.ejs +39 -0
- package/tools/templates/db/postgres/backend/java-spring/backend/src/main/resources/db/migration/V1__init.sql +17 -0
- package/tools/templates/db/postgres/backend/node-express/backend/.env.example +10 -0
- package/tools/templates/db/postgres/backend/node-express/backend/package.json.ejs +32 -0
- package/tools/templates/db/postgres/backend/node-express/backend/prisma/schema.prisma.ejs +39 -0
- package/tools/templates/frontend/angular/frontend/.env.example +9 -0
- package/tools/templates/frontend/angular/frontend/angular.json +66 -0
- package/tools/templates/frontend/angular/frontend/package.json.ejs +31 -0
- package/tools/templates/frontend/angular/frontend/src/app/app.component.ts +30 -0
- package/tools/templates/frontend/angular/frontend/src/app/app.routes.ts +18 -0
- package/tools/templates/frontend/angular/frontend/src/app/components/home.component.ts +24 -0
- package/tools/templates/frontend/angular/frontend/src/app/services/api.service.ts +48 -0
- package/tools/templates/frontend/angular/frontend/src/favicon.ico +1 -0
- package/tools/templates/frontend/angular/frontend/src/index.html +13 -0
- package/tools/templates/frontend/angular/frontend/src/main.ts +10 -0
- package/tools/templates/frontend/angular/frontend/src/styles.css +31 -0
- package/tools/templates/frontend/angular/frontend/tsconfig.app.json +9 -0
- package/tools/templates/frontend/angular/frontend/tsconfig.json +27 -0
- package/tools/templates/frontend/nextjs/frontend/.env.example +9 -0
- package/tools/templates/frontend/nextjs/frontend/next.config.js +37 -0
- package/tools/templates/frontend/nextjs/frontend/package.json.ejs +25 -0
- package/tools/templates/frontend/nextjs/frontend/src/app/globals.css +31 -0
- package/tools/templates/frontend/nextjs/frontend/src/app/layout.tsx +36 -0
- package/tools/templates/frontend/nextjs/frontend/src/app/page.tsx +19 -0
- package/tools/templates/frontend/nextjs/frontend/src/lib/api.ts +45 -0
- package/tools/templates/frontend/nextjs/frontend/tsconfig.json +27 -0
- package/tools/templates/frontend/react/frontend/.env.example +9 -0
- package/tools/templates/frontend/react/frontend/.eslintrc.json +32 -0
- package/tools/templates/frontend/react/frontend/.prettierrc +10 -0
- package/tools/templates/frontend/react/frontend/Dockerfile +37 -0
- package/tools/templates/frontend/react/frontend/README.md +54 -0
- package/tools/templates/frontend/react/frontend/index.html +13 -0
- package/tools/templates/frontend/react/frontend/nginx.conf +35 -0
- package/tools/templates/frontend/react/frontend/package.json.ejs +41 -0
- package/tools/templates/frontend/react/frontend/public/vite.svg +4 -0
- package/tools/templates/frontend/react/frontend/src/App.css +65 -0
- package/tools/templates/frontend/react/frontend/src/App.jsx +41 -0
- package/tools/templates/frontend/react/frontend/src/assets/react.svg +7 -0
- package/tools/templates/frontend/react/frontend/src/components/ErrorBoundary.jsx +62 -0
- package/tools/templates/frontend/react/frontend/src/components/Home.jsx +58 -0
- package/tools/templates/frontend/react/frontend/src/components/__tests__/Home.test.jsx +74 -0
- package/tools/templates/frontend/react/frontend/src/index.css +31 -0
- package/tools/templates/frontend/react/frontend/src/lib/api.js +42 -0
- package/tools/templates/frontend/react/frontend/src/lib/env.js +58 -0
- package/tools/templates/frontend/react/frontend/src/main.jsx +16 -0
- package/tools/templates/frontend/react/frontend/src/setupTests.js +8 -0
- package/tools/templates/frontend/react/frontend/vite.config.js +30 -0
- package/tools/templates/frontend/react/frontend/vitest.config.js +20 -0
- package/tools/templates/frontend/svelte/frontend/.env.example +9 -0
- package/tools/templates/frontend/svelte/frontend/package.json.ejs +21 -0
- package/tools/templates/frontend/svelte/frontend/src/app.html +12 -0
- package/tools/templates/frontend/svelte/frontend/src/lib/api.ts +45 -0
- package/tools/templates/frontend/svelte/frontend/src/routes/+layout.svelte +56 -0
- package/tools/templates/frontend/svelte/frontend/src/routes/+page.svelte +20 -0
- package/tools/templates/frontend/svelte/frontend/static/favicon.png +1 -0
- package/tools/templates/frontend/svelte/frontend/svelte.config.js +10 -0
- package/tools/templates/frontend/svelte/frontend/vite.config.js +9 -0
- package/tools/templates/frontend/vue/frontend/.env.example +9 -0
- package/tools/templates/frontend/vue/frontend/index.html +13 -0
- package/tools/templates/frontend/vue/frontend/package.json.ejs +20 -0
- package/tools/templates/frontend/vue/frontend/src/App.vue +60 -0
- package/tools/templates/frontend/vue/frontend/src/lib/api.js +42 -0
- package/tools/templates/frontend/vue/frontend/src/main.js +33 -0
- package/tools/templates/frontend/vue/frontend/src/views/ApiTest.vue +39 -0
- package/tools/templates/frontend/vue/frontend/src/views/Home.vue +30 -0
- package/tools/templates/frontend/vue/frontend/vite.config.js +9 -0
- package/tools/templates/modules/admin/backend/java-spring/backend/src/main/java/com/app/controller/AdminController.java +41 -0
- package/tools/templates/modules/admin/backend/java-spring/backend/src/main/java/com/app/entity/User.java +55 -0
- package/tools/templates/modules/admin/backend/java-spring/backend/src/main/java/com/app/repository/UserRepository.java +8 -0
- package/tools/templates/modules/admin/frontend/svelte/frontend/src/routes/dashboard/+page.svelte +93 -0
- package/tools/templates/modules/auth/backend/node-express/backend/src/middleware/auth.ts +42 -0
- package/tools/templates/modules/auth/frontend/svelte/frontend/src/hooks.client.ts +3 -0
- package/tools/templates/modules/auth/frontend/svelte/frontend/src/lib/auth.ts +104 -0
- package/tools/templates/modules/auth/frontend/svelte/frontend/src/routes/callback/+page.svelte +18 -0
- package/tools/templates/modules/auth/frontend/svelte/frontend/src/routes/login/+page.svelte +12 -0
- package/tools/templates/modules/chatbot/backend/node-express/backend/src/index.ts.ejs +69 -0
- package/tools/templates/modules/chatbot/backend/node-express/backend/src/routes/chatbot.ts.ejs +37 -0
- package/tools/templates/modules/chatbot/backend/node-express/backend/src/services/chatbotService.ts +124 -0
- package/tools/templates/modules/chatbot/backend/python-fastapi/backend/app/main.py.ejs +69 -0
- package/tools/templates/modules/chatbot/backend/python-fastapi/backend/app/routes/chatbot.py +38 -0
- package/tools/templates/modules/chatbot/backend/python-fastapi/backend/app/services/chatbot_service.py +123 -0
- package/tools/templates/modules/chatbot/backend/python-fastapi/backend/requirements.txt +1 -0
- package/tools/templates/modules/chatbot/frontend/react/frontend/src/App.jsx.ejs +74 -0
- package/tools/templates/modules/chatbot/frontend/react/frontend/src/components/Chatbot.css +198 -0
- package/tools/templates/modules/chatbot/frontend/react/frontend/src/components/Chatbot.jsx +113 -0
- package/tools/templates/modules/contact/backend/java-spring/backend/src/main/java/com/app/controller/ContactController.java +29 -0
- package/tools/templates/modules/contact/backend/java-spring/backend/src/main/java/com/app/entity/ContactMessage.java +66 -0
- package/tools/templates/modules/contact/backend/java-spring/backend/src/main/java/com/app/repository/ContactMessageRepository.java +8 -0
- package/tools/templates/modules/contact/backend/java-spring/backend/src/main/resources/db/migration/V2__contact.sql +7 -0
- package/tools/templates/modules/contact/frontend/svelte/frontend/src/routes/contact/+page.svelte +80 -0
- package/tools/templates/modules/payments/backend/java-spring/backend/src/main/java/com/app/controller/PaymentController.java +69 -0
- package/tools/templates/modules/payments/backend/node-express/backend/src/routes/payments.ts +30 -0
- package/tools/templates/modules/payments/backend/node-express/backend/src/routes/webhook.ts +36 -0
- package/tools/templates/modules/payments/frontend/svelte/frontend/src/lib/payments.ts +28 -0
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import re
|
|
3
|
+
import aiohttp
|
|
4
|
+
from typing import List, Dict
|
|
5
|
+
|
|
6
|
+
class ChatbotService:
|
|
7
|
+
def __init__(self):
|
|
8
|
+
self.openai_api_key = os.getenv("OPENAI_API_KEY")
|
|
9
|
+
self.anthropic_api_key = os.getenv("ANTHROPIC_API_KEY")
|
|
10
|
+
|
|
11
|
+
async def generate_response(self, message: str, history: List[Dict]) -> str:
|
|
12
|
+
"""
|
|
13
|
+
Generate a chatbot response. Tries AI providers first,
|
|
14
|
+
falls back to basic responses if no API keys configured.
|
|
15
|
+
"""
|
|
16
|
+
# Try LLM providers in order of preference
|
|
17
|
+
if self.anthropic_api_key:
|
|
18
|
+
return await self._generate_anthropic_response(message, history)
|
|
19
|
+
|
|
20
|
+
if self.openai_api_key:
|
|
21
|
+
return await self._generate_openai_response(message, history)
|
|
22
|
+
|
|
23
|
+
# Fallback to basic rule-based responses
|
|
24
|
+
return self._generate_basic_response(message)
|
|
25
|
+
|
|
26
|
+
async def _generate_openai_response(self, message: str, history: List[Dict]) -> str:
|
|
27
|
+
"""Generate response using OpenAI API"""
|
|
28
|
+
try:
|
|
29
|
+
async with aiohttp.ClientSession() as session:
|
|
30
|
+
messages = [
|
|
31
|
+
{"role": "system", "content": "You are a helpful assistant for this web application. Be concise and friendly."}
|
|
32
|
+
]
|
|
33
|
+
messages.extend([{"role": msg.get("role"), "content": msg.get("content")} for msg in history])
|
|
34
|
+
messages.append({"role": "user", "content": message})
|
|
35
|
+
|
|
36
|
+
async with session.post(
|
|
37
|
+
"https://api.openai.com/v1/chat/completions",
|
|
38
|
+
headers={
|
|
39
|
+
"Content-Type": "application/json",
|
|
40
|
+
"Authorization": f"Bearer {self.openai_api_key}"
|
|
41
|
+
},
|
|
42
|
+
json={
|
|
43
|
+
"model": "gpt-4o-mini",
|
|
44
|
+
"messages": messages,
|
|
45
|
+
"max_tokens": 500,
|
|
46
|
+
"temperature": 0.7
|
|
47
|
+
}
|
|
48
|
+
) as response:
|
|
49
|
+
if response.status != 200:
|
|
50
|
+
raise Exception(f"OpenAI API error: {response.status}")
|
|
51
|
+
|
|
52
|
+
data = await response.json()
|
|
53
|
+
return data["choices"][0]["message"]["content"]
|
|
54
|
+
except Exception as e:
|
|
55
|
+
print(f"OpenAI error: {e}")
|
|
56
|
+
return self._generate_basic_response(message)
|
|
57
|
+
|
|
58
|
+
async def _generate_anthropic_response(self, message: str, history: List[Dict]) -> str:
|
|
59
|
+
"""Generate response using Anthropic API"""
|
|
60
|
+
try:
|
|
61
|
+
async with aiohttp.ClientSession() as session:
|
|
62
|
+
messages = []
|
|
63
|
+
messages.extend([{"role": msg.get("role"), "content": msg.get("content")} for msg in history])
|
|
64
|
+
messages.append({"role": "user", "content": message})
|
|
65
|
+
|
|
66
|
+
async with session.post(
|
|
67
|
+
"https://api.anthropic.com/v1/messages",
|
|
68
|
+
headers={
|
|
69
|
+
"Content-Type": "application/json",
|
|
70
|
+
"x-api-key": self.anthropic_api_key,
|
|
71
|
+
"anthropic-version": "2023-06-01"
|
|
72
|
+
},
|
|
73
|
+
json={
|
|
74
|
+
"model": "claude-3-5-haiku-20241022",
|
|
75
|
+
"max_tokens": 500,
|
|
76
|
+
"system": "You are a helpful assistant for this web application. Be concise and friendly.",
|
|
77
|
+
"messages": messages
|
|
78
|
+
}
|
|
79
|
+
) as response:
|
|
80
|
+
if response.status != 200:
|
|
81
|
+
raise Exception(f"Anthropic API error: {response.status}")
|
|
82
|
+
|
|
83
|
+
data = await response.json()
|
|
84
|
+
return data["content"][0]["text"]
|
|
85
|
+
except Exception as e:
|
|
86
|
+
print(f"Anthropic error: {e}")
|
|
87
|
+
return self._generate_basic_response(message)
|
|
88
|
+
|
|
89
|
+
def _generate_basic_response(self, message: str) -> str:
|
|
90
|
+
"""Generate basic rule-based response (no API key required)"""
|
|
91
|
+
lower_message = message.lower()
|
|
92
|
+
|
|
93
|
+
# Greeting patterns
|
|
94
|
+
if re.search(r'(hello|hi|hey|greetings)', lower_message, re.IGNORECASE):
|
|
95
|
+
return "Hello! I'm here to help. You can ask me questions about this application or configure me with an AI API key for more advanced responses."
|
|
96
|
+
|
|
97
|
+
# Help patterns
|
|
98
|
+
if re.search(r'(help|what can you do|how do you work)', lower_message, re.IGNORECASE):
|
|
99
|
+
return "I can answer questions and assist you with this application. Currently, I'm running in basic mode. To unlock AI-powered responses, add OPENAI_API_KEY or ANTHROPIC_API_KEY to your backend .env file."
|
|
100
|
+
|
|
101
|
+
# Feature questions
|
|
102
|
+
if re.search(r'(feature|what|how)', lower_message, re.IGNORECASE):
|
|
103
|
+
return "This is a full-stack web application with authentication, database integration, and more. Feel free to explore the codebase or ask me specific questions!"
|
|
104
|
+
|
|
105
|
+
# API/Configuration questions
|
|
106
|
+
if re.search(r'(api|configure|setup|install)', lower_message, re.IGNORECASE):
|
|
107
|
+
return """To configure AI responses:
|
|
108
|
+
|
|
109
|
+
1. Get an API key from OpenAI or Anthropic
|
|
110
|
+
2. Add it to backend/.env:
|
|
111
|
+
OPENAI_API_KEY=sk-...
|
|
112
|
+
or
|
|
113
|
+
ANTHROPIC_API_KEY=sk-ant-...
|
|
114
|
+
3. Restart the backend
|
|
115
|
+
|
|
116
|
+
Then I'll be powered by AI!"""
|
|
117
|
+
|
|
118
|
+
# Thanks/Goodbye
|
|
119
|
+
if re.search(r'(thank|thanks|bye|goodbye)', lower_message, re.IGNORECASE):
|
|
120
|
+
return "You're welcome! Let me know if you need anything else."
|
|
121
|
+
|
|
122
|
+
# Default fallback
|
|
123
|
+
return "I received your message! I'm currently in basic mode with limited responses. For more intelligent conversations, configure an AI API key (OpenAI or Anthropic) in your backend environment variables."
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
aiohttp==3.9.1
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react'
|
|
2
|
+
import reactLogo from './assets/react.svg'
|
|
3
|
+
import viteLogo from '/vite.svg'
|
|
4
|
+
import './App.css'
|
|
5
|
+
import { apiFetch } from './lib/api'
|
|
6
|
+
import Chatbot from './components/Chatbot'
|
|
7
|
+
|
|
8
|
+
function App() {
|
|
9
|
+
const [count, setCount] = useState(0)
|
|
10
|
+
const [apiStatus, setApiStatus] = useState('checking')
|
|
11
|
+
const [apiMessage, setApiMessage] = useState('')
|
|
12
|
+
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
// Test API connection on page load
|
|
15
|
+
apiFetch('/api/example')
|
|
16
|
+
.then(data => {
|
|
17
|
+
setApiStatus('connected')
|
|
18
|
+
setApiMessage(data.message)
|
|
19
|
+
})
|
|
20
|
+
.catch(err => {
|
|
21
|
+
setApiStatus('error')
|
|
22
|
+
setApiMessage(err.message)
|
|
23
|
+
})
|
|
24
|
+
}, [])
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<>
|
|
28
|
+
<div>
|
|
29
|
+
<a href="https://vite.dev" target="_blank">
|
|
30
|
+
<img src={viteLogo} className="logo" alt="Vite logo" />
|
|
31
|
+
</a>
|
|
32
|
+
<a href="https://react.dev" target="_blank">
|
|
33
|
+
<img src={reactLogo} className="logo react" alt="React logo" />
|
|
34
|
+
</a>
|
|
35
|
+
</div>
|
|
36
|
+
<h1>Vite + React</h1>
|
|
37
|
+
<div className="card">
|
|
38
|
+
<button onClick={() => setCount((count) => count + 1)}>
|
|
39
|
+
count is {count}
|
|
40
|
+
</button>
|
|
41
|
+
<p>
|
|
42
|
+
Edit <code>src/App.jsx</code> and save to test HMR
|
|
43
|
+
</p>
|
|
44
|
+
</div>
|
|
45
|
+
<div className="api-status">
|
|
46
|
+
<h3>Backend API Status</h3>
|
|
47
|
+
{apiStatus === 'checking' && <p>⏳ Checking connection...</p>}
|
|
48
|
+
{apiStatus === 'connected' && (
|
|
49
|
+
<>
|
|
50
|
+
<p style={{ color: 'green' }}>✅ Connected to backend!</p>
|
|
51
|
+
<p style={{ fontSize: '0.9em', color: '#888' }}>{apiMessage}</p>
|
|
52
|
+
</>
|
|
53
|
+
)}
|
|
54
|
+
{apiStatus === 'error' && (
|
|
55
|
+
<>
|
|
56
|
+
<p style={{ color: 'red' }}>❌ Backend not connected</p>
|
|
57
|
+
<p style={{ fontSize: '0.9em', color: '#888' }}>{apiMessage}</p>
|
|
58
|
+
<p style={{ fontSize: '0.85em', marginTop: '8px' }}>
|
|
59
|
+
Make sure the backend server is running on port 8080
|
|
60
|
+
</p>
|
|
61
|
+
</>
|
|
62
|
+
)}
|
|
63
|
+
</div>
|
|
64
|
+
<p className="read-the-docs">
|
|
65
|
+
Click on the Vite and React logos to learn more
|
|
66
|
+
</p>
|
|
67
|
+
|
|
68
|
+
{/* Chatbot component */}
|
|
69
|
+
<Chatbot />
|
|
70
|
+
</>
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export default App
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
.chatbot-toggle {
|
|
2
|
+
position: fixed;
|
|
3
|
+
bottom: 24px;
|
|
4
|
+
right: 24px;
|
|
5
|
+
width: 60px;
|
|
6
|
+
height: 60px;
|
|
7
|
+
border-radius: 50%;
|
|
8
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
9
|
+
color: white;
|
|
10
|
+
border: none;
|
|
11
|
+
font-size: 24px;
|
|
12
|
+
cursor: pointer;
|
|
13
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
14
|
+
transition: transform 0.2s, box-shadow 0.2s;
|
|
15
|
+
z-index: 1000;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.chatbot-toggle:hover {
|
|
19
|
+
transform: scale(1.05);
|
|
20
|
+
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.2);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.chatbot-window {
|
|
24
|
+
position: fixed;
|
|
25
|
+
bottom: 100px;
|
|
26
|
+
right: 24px;
|
|
27
|
+
width: 380px;
|
|
28
|
+
height: 550px;
|
|
29
|
+
background: white;
|
|
30
|
+
border-radius: 12px;
|
|
31
|
+
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.15);
|
|
32
|
+
display: flex;
|
|
33
|
+
flex-direction: column;
|
|
34
|
+
z-index: 1000;
|
|
35
|
+
overflow: hidden;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.chatbot-header {
|
|
39
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
40
|
+
color: white;
|
|
41
|
+
padding: 16px 20px;
|
|
42
|
+
display: flex;
|
|
43
|
+
justify-content: space-between;
|
|
44
|
+
align-items: center;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.chatbot-header h3 {
|
|
48
|
+
margin: 0;
|
|
49
|
+
font-size: 18px;
|
|
50
|
+
font-weight: 600;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.chatbot-header button {
|
|
54
|
+
background: transparent;
|
|
55
|
+
border: none;
|
|
56
|
+
color: white;
|
|
57
|
+
font-size: 24px;
|
|
58
|
+
cursor: pointer;
|
|
59
|
+
padding: 0;
|
|
60
|
+
width: 30px;
|
|
61
|
+
height: 30px;
|
|
62
|
+
display: flex;
|
|
63
|
+
align-items: center;
|
|
64
|
+
justify-content: center;
|
|
65
|
+
opacity: 0.8;
|
|
66
|
+
transition: opacity 0.2s;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.chatbot-header button:hover {
|
|
70
|
+
opacity: 1;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.chatbot-messages {
|
|
74
|
+
flex: 1;
|
|
75
|
+
overflow-y: auto;
|
|
76
|
+
padding: 16px;
|
|
77
|
+
background: #f7f9fc;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.message {
|
|
81
|
+
margin-bottom: 12px;
|
|
82
|
+
display: flex;
|
|
83
|
+
animation: fadeIn 0.3s;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
@keyframes fadeIn {
|
|
87
|
+
from { opacity: 0; transform: translateY(10px); }
|
|
88
|
+
to { opacity: 1; transform: translateY(0); }
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.message-user {
|
|
92
|
+
justify-content: flex-end;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.message-assistant {
|
|
96
|
+
justify-content: flex-start;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.message-content {
|
|
100
|
+
max-width: 75%;
|
|
101
|
+
padding: 10px 14px;
|
|
102
|
+
border-radius: 12px;
|
|
103
|
+
word-wrap: break-word;
|
|
104
|
+
line-height: 1.4;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.message-user .message-content {
|
|
108
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
109
|
+
color: white;
|
|
110
|
+
border-bottom-right-radius: 4px;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.message-assistant .message-content {
|
|
114
|
+
background: white;
|
|
115
|
+
color: #333;
|
|
116
|
+
border-bottom-left-radius: 4px;
|
|
117
|
+
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.typing-indicator {
|
|
121
|
+
display: flex;
|
|
122
|
+
gap: 4px;
|
|
123
|
+
padding: 12px 16px;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.typing-indicator span {
|
|
127
|
+
width: 8px;
|
|
128
|
+
height: 8px;
|
|
129
|
+
border-radius: 50%;
|
|
130
|
+
background: #999;
|
|
131
|
+
animation: typing 1.4s infinite;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.typing-indicator span:nth-child(2) {
|
|
135
|
+
animation-delay: 0.2s;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.typing-indicator span:nth-child(3) {
|
|
139
|
+
animation-delay: 0.4s;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
@keyframes typing {
|
|
143
|
+
0%, 60%, 100% { transform: translateY(0); }
|
|
144
|
+
30% { transform: translateY(-10px); }
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
.chatbot-input {
|
|
148
|
+
display: flex;
|
|
149
|
+
gap: 8px;
|
|
150
|
+
padding: 16px;
|
|
151
|
+
background: white;
|
|
152
|
+
border-top: 1px solid #e5e7eb;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
.chatbot-input input {
|
|
156
|
+
flex: 1;
|
|
157
|
+
padding: 10px 14px;
|
|
158
|
+
border: 1px solid #d1d5db;
|
|
159
|
+
border-radius: 8px;
|
|
160
|
+
font-size: 14px;
|
|
161
|
+
outline: none;
|
|
162
|
+
transition: border-color 0.2s;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
.chatbot-input input:focus {
|
|
166
|
+
border-color: #667eea;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
.chatbot-input button {
|
|
170
|
+
padding: 10px 20px;
|
|
171
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
172
|
+
color: white;
|
|
173
|
+
border: none;
|
|
174
|
+
border-radius: 8px;
|
|
175
|
+
font-size: 14px;
|
|
176
|
+
font-weight: 600;
|
|
177
|
+
cursor: pointer;
|
|
178
|
+
transition: opacity 0.2s;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
.chatbot-input button:hover:not(:disabled) {
|
|
182
|
+
opacity: 0.9;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
.chatbot-input button:disabled {
|
|
186
|
+
opacity: 0.5;
|
|
187
|
+
cursor: not-allowed;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/* Mobile responsive */
|
|
191
|
+
@media (max-width: 480px) {
|
|
192
|
+
.chatbot-window {
|
|
193
|
+
width: calc(100vw - 24px);
|
|
194
|
+
height: calc(100vh - 120px);
|
|
195
|
+
right: 12px;
|
|
196
|
+
bottom: 90px;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { useState, useRef, useEffect } from 'react';
|
|
2
|
+
import { apiFetch } from '../lib/api';
|
|
3
|
+
import './Chatbot.css';
|
|
4
|
+
|
|
5
|
+
export default function Chatbot() {
|
|
6
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
7
|
+
const [messages, setMessages] = useState([
|
|
8
|
+
{ role: 'assistant', content: 'Hi! How can I help you today?' }
|
|
9
|
+
]);
|
|
10
|
+
const [input, setInput] = useState('');
|
|
11
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
12
|
+
const messagesEndRef = useRef(null);
|
|
13
|
+
const isMountedRef = useRef(true);
|
|
14
|
+
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
return () => {
|
|
17
|
+
isMountedRef.current = false;
|
|
18
|
+
};
|
|
19
|
+
}, []);
|
|
20
|
+
|
|
21
|
+
const scrollToBottom = () => {
|
|
22
|
+
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
scrollToBottom();
|
|
27
|
+
}, [messages]);
|
|
28
|
+
|
|
29
|
+
const handleSubmit = async (e) => {
|
|
30
|
+
e.preventDefault();
|
|
31
|
+
if (!input.trim() || isLoading) return;
|
|
32
|
+
|
|
33
|
+
const userMessage = input.trim();
|
|
34
|
+
setInput('');
|
|
35
|
+
setMessages(prev => [...prev, { role: 'user', content: userMessage }]);
|
|
36
|
+
setIsLoading(true);
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
const response = await apiFetch('/api/chatbot', {
|
|
40
|
+
method: 'POST',
|
|
41
|
+
body: JSON.stringify({ message: userMessage, history: messages })
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
if (isMountedRef.current) {
|
|
45
|
+
setMessages(prev => [...prev, { role: 'assistant', content: response.message }]);
|
|
46
|
+
}
|
|
47
|
+
} catch (error) {
|
|
48
|
+
console.error('Chatbot error:', error);
|
|
49
|
+
if (isMountedRef.current) {
|
|
50
|
+
setMessages(prev => [...prev, {
|
|
51
|
+
role: 'assistant',
|
|
52
|
+
content: 'Sorry, I encountered an error. Please try again.'
|
|
53
|
+
}]);
|
|
54
|
+
}
|
|
55
|
+
} finally {
|
|
56
|
+
if (isMountedRef.current) {
|
|
57
|
+
setIsLoading(false);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<>
|
|
64
|
+
{/* Chat Button */}
|
|
65
|
+
<button
|
|
66
|
+
className="chatbot-toggle"
|
|
67
|
+
onClick={() => setIsOpen(!isOpen)}
|
|
68
|
+
aria-label="Toggle chatbot"
|
|
69
|
+
>
|
|
70
|
+
{isOpen ? '✕' : '💬'}
|
|
71
|
+
</button>
|
|
72
|
+
|
|
73
|
+
{/* Chat Window */}
|
|
74
|
+
{isOpen && (
|
|
75
|
+
<div className="chatbot-window">
|
|
76
|
+
<div className="chatbot-header">
|
|
77
|
+
<h3>Chat Assistant</h3>
|
|
78
|
+
<button onClick={() => setIsOpen(false)} aria-label="Close chat">✕</button>
|
|
79
|
+
</div>
|
|
80
|
+
|
|
81
|
+
<div className="chatbot-messages">
|
|
82
|
+
{messages.map((msg, idx) => (
|
|
83
|
+
<div key={idx} className={`message message-${msg.role}`}>
|
|
84
|
+
<div className="message-content">{msg.content}</div>
|
|
85
|
+
</div>
|
|
86
|
+
))}
|
|
87
|
+
{isLoading && (
|
|
88
|
+
<div className="message message-assistant">
|
|
89
|
+
<div className="message-content typing-indicator">
|
|
90
|
+
<span></span><span></span><span></span>
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
)}
|
|
94
|
+
<div ref={messagesEndRef} />
|
|
95
|
+
</div>
|
|
96
|
+
|
|
97
|
+
<form onSubmit={handleSubmit} className="chatbot-input">
|
|
98
|
+
<input
|
|
99
|
+
type="text"
|
|
100
|
+
value={input}
|
|
101
|
+
onChange={(e) => setInput(e.target.value)}
|
|
102
|
+
placeholder="Type your message..."
|
|
103
|
+
disabled={isLoading}
|
|
104
|
+
/>
|
|
105
|
+
<button type="submit" disabled={isLoading || !input.trim()}>
|
|
106
|
+
Send
|
|
107
|
+
</button>
|
|
108
|
+
</form>
|
|
109
|
+
</div>
|
|
110
|
+
)}
|
|
111
|
+
</>
|
|
112
|
+
);
|
|
113
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
package com.app.controller;
|
|
2
|
+
|
|
3
|
+
import com.app.entity.ContactMessage;
|
|
4
|
+
import com.app.repository.ContactMessageRepository;
|
|
5
|
+
import org.springframework.beans.factory.annotation.Autowired;
|
|
6
|
+
import org.springframework.http.ResponseEntity;
|
|
7
|
+
import org.springframework.web.bind.annotation.*;
|
|
8
|
+
|
|
9
|
+
import java.util.Map;
|
|
10
|
+
|
|
11
|
+
@RestController
|
|
12
|
+
@RequestMapping("/api")
|
|
13
|
+
public class ContactController {
|
|
14
|
+
|
|
15
|
+
@Autowired
|
|
16
|
+
private ContactMessageRepository contactMessageRepository;
|
|
17
|
+
|
|
18
|
+
@PostMapping("/contact")
|
|
19
|
+
public ResponseEntity<?> submitContact(@RequestBody Map<String, String> body) {
|
|
20
|
+
ContactMessage contact = new ContactMessage();
|
|
21
|
+
contact.setName(body.get("name"));
|
|
22
|
+
contact.setEmail(body.get("email"));
|
|
23
|
+
contact.setMessage(body.get("message"));
|
|
24
|
+
|
|
25
|
+
contactMessageRepository.save(contact);
|
|
26
|
+
|
|
27
|
+
return ResponseEntity.ok(Map.of("success", true));
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
package com.app.entity;
|
|
2
|
+
|
|
3
|
+
import jakarta.persistence.*;
|
|
4
|
+
import java.time.Instant;
|
|
5
|
+
|
|
6
|
+
@Entity
|
|
7
|
+
@Table(name = "contact_messages")
|
|
8
|
+
public class ContactMessage {
|
|
9
|
+
|
|
10
|
+
@Id
|
|
11
|
+
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
|
12
|
+
private Long id;
|
|
13
|
+
|
|
14
|
+
@Column(length = 120)
|
|
15
|
+
private String name;
|
|
16
|
+
|
|
17
|
+
@Column(length = 255)
|
|
18
|
+
private String email;
|
|
19
|
+
|
|
20
|
+
@Column(columnDefinition = "TEXT")
|
|
21
|
+
private String message;
|
|
22
|
+
|
|
23
|
+
@Column(name = "created_at", nullable = false, updatable = false)
|
|
24
|
+
private Instant createdAt = Instant.now();
|
|
25
|
+
|
|
26
|
+
// Getters and setters
|
|
27
|
+
public Long getId() {
|
|
28
|
+
return id;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
public void setId(Long id) {
|
|
32
|
+
this.id = id;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
public String getName() {
|
|
36
|
+
return name;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
public void setName(String name) {
|
|
40
|
+
this.name = name;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
public String getEmail() {
|
|
44
|
+
return email;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
public void setEmail(String email) {
|
|
48
|
+
this.email = email;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
public String getMessage() {
|
|
52
|
+
return message;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
public void setMessage(String message) {
|
|
56
|
+
this.message = message;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
public Instant getCreatedAt() {
|
|
60
|
+
return createdAt;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
public void setCreatedAt(Instant createdAt) {
|
|
64
|
+
this.createdAt = createdAt;
|
|
65
|
+
}
|
|
66
|
+
}
|