diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000000000000000000000000000000000000..40be386b981d8564ef04539edebb44dc6a389d39 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,40 @@ +include: + - project: osl/code/org.etsi.osl.main + ref: main + file: + - ci-templates/default.yml + - ci-templates/build.yml + rules: + - if: '$CI_COMMIT_REF_NAME == "main"' + + - project: osl/code/org.etsi.osl.main + ref: develop + file: + - ci-templates/default.yml + - ci-templates/build.yml + rules: + - if: '$CI_COMMIT_REF_NAME == "develop"' + + - project: osl/code/org.etsi.osl.main + ref: $CI_COMMIT_REF_NAME + file: + - ci-templates/default.yml + - ci-templates/build.yml + rules: + - if: '$CI_COMMIT_REF_PROTECTED == "true" && $CI_COMMIT_REF_NAME != "main" && $CI_COMMIT_REF_NAME != "develop"' + + - project: osl/code/org.etsi.osl.main + ref: develop + file: + - ci-templates/default.yml + - ci-templates/build_unprotected.yml + rules: + - if: '$CI_COMMIT_REF_NAME != "main" && $CI_COMMIT_REF_NAME != "develop" && $CI_COMMIT_REF_PROTECTED == "false"' + +maven_build: + extends: .maven_build + +docker_build: + extends: .docker_build + needs: + - maven_build diff --git a/Dockerfile b/Dockerfile index aa6f29869aefce72c3e546d0e1082405ff3c6c3a..440fed70533ede2745cd23d2cdc88deff3c3dbfb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,32 +1,32 @@ -# Build stage +# ---------- Stage 1: Build ---------- FROM maven:3.9-eclipse-temurin-17 AS builder +WORKDIR /app -WORKDIR /build - -# Copy pom.xml and download dependencies +# Copy pom.xml and download dependencies first (cache layer) COPY pom.xml . -RUN mvn dependency:go-offline +RUN mvn dependency:go-offline -B -# Copy source code +# Copy the rest of the source code COPY src ./src -# Build application +# Build the Spring Boot executable JAR RUN mvn clean package -DskipTests -# Runtime stage +# ---------- Stage 2: Runtime ---------- FROM eclipse-temurin:17-jre-alpine -WORKDIR /app +# Create application directory +RUN mkdir -p /opt/openslice/lib/ -# Copy built JAR from builder stage -COPY --from=builder /build/target/org.etsi.osl.mcp.backend-0.0.1-SNAPSHOT.jar app.jar +# Copy the built JAR from the builder stage +COPY --from=builder /app/target/org.etsi.osl.mcp.backend-0.0.1-SNAPSHOT.jar /opt/openslice/lib/app.jar # Expose port EXPOSE 11880 # Health check -HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ +HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \ CMD wget --no-verbose --tries=1 --spider http://localhost:11880/actuator/health || exit 1 # Run application -ENTRYPOINT ["java", "-jar", "app.jar"] +CMD ["java", "-jar", "/opt/openslice/lib/app.jar"] diff --git a/ci_settings.xml b/ci_settings.xml new file mode 100644 index 0000000000000000000000000000000000000000..69ad06ed6c63795d191555afde6ea2d1da4e133d --- /dev/null +++ b/ci_settings.xml @@ -0,0 +1,16 @@ + + + + gitlab-maven + + + + Job-Token + ${CI_JOB_TOKEN} + + + + + + diff --git a/docker-compose.yml b/docker-compose.yml index 58bcfa1f1ee4905df2aad31496d0542964fe05ff..cf9c10a60534c3f03c9e2b06a08eb317693e5c75 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -29,17 +29,46 @@ services: ports: - "11880:11880" environment: + # Server Configuration + SERVER_PORT: 11880 + SPRING_APPLICATION_NAME: osl-mcp-backend + + # AI Configuration SPRING_AI_OLLAMA_BASE_URL: http://ollama:11434 - SPRING_AI_MCP_CLIENT_STREAMABLE_HTTP_CONNECTIONS_OPENSLICE_SERVER_URL: http://mcp-server:13015 - SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_AUTHSERVER_ISSUER_URI: http://keycloak:8080/auth/realms/openslice - SPRING_ACTIVEMQ_BROKERURL: tcp://activemq:61616 + SPRING_AI_OLLAMA_CHAT_MODEL: gpt-oss:20b + SPRING_AI_OLLAMA_CHAT_TEMPERATURE: 0.7 + + # MCP Client Configuration + SPRING_AI_MCP_CLIENT_TYPE: SYNC + SPRING_AI_MCP_CLIENT_STREAMABLE_HTTP_CONNECTIONS_OPENSLICE_SERVER_URL: http://openslice-mcp:13015/sse + + # OAuth2/Keycloak Configuration + SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_ISSUER_URI: http://keycloak:8080/auth/realms/openslice + SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_KEYCLOAK_ISSUER_URI: http://keycloak:8080/auth/realms/openslice + SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_KEYCLOAK_CLIENT_ID: osapiWebClientId + + # ActiveMQ Configuration + SPRING_ACTIVEMQ_BROKER_URL: tcp://anartemis:61616?jms.watchTopicAdvisories=false SPRING_ACTIVEMQ_USER: artemis SPRING_ACTIVEMQ_PASSWORD: artemis + + # Logging Configuration + LOGGING_LEVEL_ROOT: INFO + LOGGING_LEVEL_OSL: DEBUG + LOGGING_LEVEL_SPRING_AI: DEBUG + depends_on: + - ollama healthcheck: test: ["CMD", "curl", "-f", "http://localhost:11880/actuator/health"] interval: 30s timeout: 10s retries: 3 + start_period: 60s volumes: ollama_data: + +networks: + default: + name: compose_back + external: true diff --git a/pom.xml b/pom.xml index 49ba05b5c5a10f0a11e045e806cd4bf58f6ac58c..e392e4f94411193c8774260483b1798fcb38ae47 100644 --- a/pom.xml +++ b/pom.xml @@ -51,6 +51,14 @@ + + org.springframework.boot + spring-boot-starter-oauth2-client + + + org.springframework.boot + spring-boot-starter-oauth2-resource-server + org.springframework.boot spring-boot-starter-web @@ -73,6 +81,10 @@ spring-boot-starter-test test + + org.springframework.boot + spring-boot-starter-actuator + diff --git a/src/main/java/org/etsi/osl/mcp/backend/AuthController.java b/src/main/java/org/etsi/osl/mcp/backend/AuthController.java new file mode 100644 index 0000000000000000000000000000000000000000..1905527891f258e1fdcdf2d23e2e3c9983ab535f --- /dev/null +++ b/src/main/java/org/etsi/osl/mcp/backend/AuthController.java @@ -0,0 +1,115 @@ +package org.etsi.osl.mcp.backend; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; +import org.springframework.security.oauth2.client.annotation.RegisteredOAuth2AuthorizedClient; +import org.springframework.security.oauth2.core.oidc.user.OidcUser; +import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.util.UriComponentsBuilder; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +@RestController +public class AuthController { + + @Value("${spring.security.oauth2.client.provider.keycloak.issuer-uri:}") + private String issuerUri; + + @GetMapping("/api/user") + public Map getUserInfo(Authentication authentication, + @RegisteredOAuth2AuthorizedClient("keycloak") OAuth2AuthorizedClient authorizedClient) { + Map userInfo = new HashMap<>(); + + if (authentication != null && authentication.isAuthenticated()) { + userInfo.put("authenticated", true); + userInfo.put("name", authentication.getName()); + + String username = authentication.getName(); + if (authentication.getPrincipal() instanceof OidcUser oidcUser) { + userInfo.put("email", oidcUser.getEmail()); + if (oidcUser.getPreferredUsername() != null) { + username = oidcUser.getPreferredUsername(); + userInfo.put("preferredUsername", oidcUser.getPreferredUsername()); + } + } + userInfo.put("username", username); + + if (authorizedClient != null && authorizedClient.getAccessToken() != null) { + userInfo.put("access_token", authorizedClient.getAccessToken().getTokenValue()); + userInfo.put("token", authorizedClient.getAccessToken().getTokenValue()); + } + } else { + userInfo.put("authenticated", false); + } + + return userInfo; + } + + @PostMapping("/api/logout") + public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException { + performLogout(request, response, authentication); + } + + @GetMapping("/api/logout") + public void logoutGet(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException { + performLogout(request, response, authentication); + } + + private void performLogout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException { + if (authentication != null) { + new SecurityContextLogoutHandler().logout(request, null, authentication); + } + + if (request.getSession(false) != null) { + request.getSession().invalidate(); + } + + String logoutUrl = buildKeycloakLogoutUrl(request, authentication); + response.sendRedirect(logoutUrl); + } + + private String buildKeycloakLogoutUrl(HttpServletRequest request, Authentication authentication) { + String keycloakIssuer = issuerUri; + + if (authentication != null && authentication.getPrincipal() instanceof OidcUser oidcUser) { + if (oidcUser.getIssuer() != null) { + keycloakIssuer = oidcUser.getIssuer().toString(); + } + } + + if (keycloakIssuer == null || keycloakIssuer.isEmpty()) { + return "/welcome.html"; + } + + String baseUrl = request.getScheme() + "://" + request.getServerName(); + if ((request.getScheme().equals("http") && request.getServerPort() != 80) || + (request.getScheme().equals("https") && request.getServerPort() != 443)) { + baseUrl += ":" + request.getServerPort(); + } + + String postLogoutRedirectUri = baseUrl + "/welcome.html"; + + String keycloakLogoutUrl = keycloakIssuer + "/protocol/openid-connect/logout"; + + return UriComponentsBuilder + .fromUriString(keycloakLogoutUrl) + .queryParam("post_logout_redirect_uri", postLogoutRedirectUri) + .build() + .toUriString(); + } + + @GetMapping("/api/health") + public Map health() { + Map response = new HashMap<>(); + response.put("status", "UP"); + return response; + } +} diff --git a/src/main/java/org/etsi/osl/mcp/backend/WebSecurityConfigKeycloak.java b/src/main/java/org/etsi/osl/mcp/backend/WebSecurityConfigKeycloak.java index eb92b78a910a5f4c3abd30b81eb2d7e1cf163c5f..744cd3618d1d547aa5bf6338c85129930467b1f0 100644 --- a/src/main/java/org/etsi/osl/mcp/backend/WebSecurityConfigKeycloak.java +++ b/src/main/java/org/etsi/osl/mcp/backend/WebSecurityConfigKeycloak.java @@ -7,19 +7,70 @@ import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configurers.CsrfConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter; +import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter; import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.HttpStatusEntryPoint; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import org.springframework.security.web.util.matcher.RequestMatcher; +import org.springframework.http.HttpStatus; @Configuration @EnableWebSecurity @Profile("!testing") public class WebSecurityConfigKeycloak { - @Bean SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { - return http.authorizeHttpRequests(auth -> auth.anyRequest().permitAll()) + return http + .authorizeHttpRequests(auth -> auth + // Public static resources + .requestMatchers("/", "/index.html", "/styles.css", "/robot.svg", "/favicon.ico").permitAll() + .requestMatchers("/api/health").permitAll() + .requestMatchers("/api/user").permitAll() + .requestMatchers("/api/logout").permitAll() + .requestMatchers("/logout").permitAll() + .requestMatchers("/ask", "/api/**").authenticated() + .anyRequest().permitAll() + ) + // OAuth2 Login for browser-based authentication + .oauth2Login(oauth2 -> oauth2 + .defaultSuccessUrl("/", true) + ) + // Logout configuration + .logout(logout -> logout + .logoutUrl("/logout") + .logoutSuccessUrl("/") + .invalidateHttpSession(true) + .deleteCookies("JSESSIONID") + ) .oauth2Client(Customizer.withDefaults()) + .oauth2ResourceServer(oauth2 -> oauth2 + .jwt(jwt -> jwt.jwtAuthenticationConverter(jwtAuthenticationConverter())) + .authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED)) + ) + .exceptionHandling(exception -> exception + .defaultAuthenticationEntryPointFor( + new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED), + new AntPathRequestMatcher("/ask") + ) + ) + .sessionManagement(session -> session + .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED) + ) .csrf(CsrfConfigurer::disable) .build(); } + + @Bean + public JwtAuthenticationConverter jwtAuthenticationConverter() { + JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter(); + grantedAuthoritiesConverter.setAuthoritiesClaimName("roles"); + grantedAuthoritiesConverter.setAuthorityPrefix("ROLE_"); + + JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter(); + jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter); + return jwtAuthenticationConverter; + } } diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index d2d75f41f25c2358010fabc36cf60710eab6cc75..83819de6ff53a1adecd2858ac086e459b8d3250a 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -1,49 +1,53 @@ server: - port: 11880 + port: ${SERVER_PORT:11880} spring: application: - name: osl-mcp-backend + name: ${SPRING_APPLICATION_NAME:osl-mcp-backend} ai: ollama: - base-url: http://localhost:11434 + base-url: ${SPRING_AI_OLLAMA_BASE_URL:http://localhost:11434} chat: options: - model: gpt-oss:20b - xmodel: qwen3:8b - temperature: 0.7 + model: ${SPRING_AI_OLLAMA_CHAT_MODEL:gpt-oss:20b} + temperature: ${SPRING_AI_OLLAMA_CHAT_TEMPERATURE:0.7} mcp: client: - type: SYNC # or ASYNC + type: ${SPRING_AI_MCP_CLIENT_TYPE:SYNC} # or ASYNC streamable-http: connections: openslice-server: - url: http://localhost:13015 + url: ${SPRING_AI_MCP_CLIENT_STREAMABLE_HTTP_CONNECTIONS_OPENSLICE_SERVER_URL:http://localhost:13015} security: oauth2: + resourceserver: + jwt: + issuer-uri: ${SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_ISSUER_URI:http://keycloak:8080/auth/realms/openslice} client: registration: - authserver: - client-id: osapiWebClientId - client-secret: secret - authorization-grant-type: authorization_code - provider: authserver + keycloak: + client-id: ${SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_KEYCLOAK_CLIENT_ID:osapiWebClientId} + scope: + - openid + - profile + provider: keycloak provider: - authserver: - issuer-uri: http://keycloak:8080/auth/realms/openslice + keycloak: + issuer-uri: ${SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_KEYCLOAK_ISSUER_URI:http://keycloak:8080/auth/realms/openslice} activemq: - brokerUrl: tcp://localhost:61616?jms.watchTopicAdvisories=false - user: artemis - password: artemis + brokerUrl: ${SPRING_ACTIVEMQ_BROKER_URL:tcp://localhost:61616?jms.watchTopicAdvisories=false} + user: ${SPRING_ACTIVEMQ_USER:artemis} + password: ${SPRING_ACTIVEMQ_PASSWORD:artemis} pool: - enabled: true - max-connections: 100 + enabled: ${SPRING_ACTIVEMQ_POOL_ENABLED:true} + max-connections: ${SPRING_ACTIVEMQ_POOL_MAX_CONNECTIONS:100} packages: - trust-all: true + trust-all: ${SPRING_ACTIVEMQ_PACKAGES_TRUST_ALL:true} + logging: level: - root: INFO - org.etsi.osl.*: DEBUG - org.springframework.ai.chat.client.advisor: DEBUG \ No newline at end of file + root: ${LOGGING_LEVEL_ROOT:INFO} + org.etsi.osl.*: ${LOGGING_LEVEL_OSL:DEBUG} + org.springframework.ai.chat.client.advisor: ${LOGGING_LEVEL_SPRING_AI:DEBUG} \ No newline at end of file diff --git a/src/main/resources/public/chat.html b/src/main/resources/public/chat.html new file mode 100644 index 0000000000000000000000000000000000000000..944c0fd38d98728af4262c2bf4a17e7c5c0e639c --- /dev/null +++ b/src/main/resources/public/chat.html @@ -0,0 +1,60 @@ + + + + + + OpenSlice Chat Assistant + + + + + + + + +
+
+
+

OpenSlice AI Assistant

+ Ask me anything about your OpenSlice platform +
+
+
+
AI
+
+
Hi there! How can I help you today?
+
+
+
+
+
+
+ + +
+
+
+
+ + + diff --git a/src/main/resources/public/css/chat.css b/src/main/resources/public/css/chat.css new file mode 100644 index 0000000000000000000000000000000000000000..df841f1254ed0173949fdfe9d07c55f8a34a7163 --- /dev/null +++ b/src/main/resources/public/css/chat.css @@ -0,0 +1,307 @@ +/* OpenSlice Chat Assistant - Chat Page Styles */ + +body { + font-family: "Open Sans", sans-serif; + padding-top: 60px; + padding-bottom: 20px; + background-color: white; +} + +/* Navbar */ +.navbar-default { + background-color: rgba(245, 245, 245, 0.93); + border-bottom: 1px solid #e7e7e7; +} + +.navbar-brand img { + height: 25px; +} + +/* Chat Container */ +.chat-container { + max-width: 900px; + margin: 20px auto; + background: white; + border-radius: 8px; + box-shadow: 0 2px 10px rgba(0,0,0,0.1); + overflow: hidden; +} + +/* Chat Header */ +.chat-header { + background-color: #7EADDB; + color: white; + padding: 20px; + border-bottom: 1px solid #6589AB; +} + +.chat-header h4 { + margin: 0; + font-weight: 600; +} + +.chat-header .online-indicator { + display: inline-block; + width: 8px; + height: 8px; + background-color: #5cb85c; + border-radius: 50%; + margin-right: 5px; +} + +/* Chat Messages */ +.chat-messages { + height: 500px; + overflow-y: auto; + padding: 20px; + background-color: #f9f9f9; +} + +.message { + margin-bottom: 20px; + display: flex; + align-items: flex-start; + clear: both; +} + +.message.incoming { + flex-direction: row; + justify-content: flex-start; + padding-right: 80px; +} + +.message.outgoing { + flex-direction: row-reverse; + justify-content: flex-start; + padding-left: 80px; +} + +.message .avatar { + width: 40px; + height: 40px; + border-radius: 50%; + background-color: #7EADDB; + display: flex; + align-items: center; + justify-content: center; + color: white; + font-weight: bold; + flex-shrink: 0; +} + +.message.incoming .avatar { + margin-right: 12px; +} + +.message.outgoing .avatar { + margin-left: 12px; + background-color: #6589AB; +} + +.message-wrapper { + display: flex; + flex-direction: column; + max-width: 66%; + min-width: fit-content; +} + +.message.incoming .message-wrapper { + align-items: flex-start; +} + +.message.outgoing .message-wrapper { + align-items: flex-end; +} + +.message-content { + max-width: 100%; + padding: 12px 16px; + border-radius: 8px; + word-wrap: break-word; + white-space: pre-wrap; + line-height: 1.5; +} + +.message-content p { + margin: 0; +} + +.message-content p:last-child { + margin-bottom: 0; +} + +.message-content p + p { + margin-top: 10px; +} + +.message-content code { + background-color: rgba(0, 0, 0, 0.05); + padding: 2px 4px; + border-radius: 3px; + font-family: monospace; + font-size: 13px; +} + +.message-content pre { + background-color: rgba(0, 0, 0, 0.05); + padding: 10px; + border-radius: 4px; + overflow-x: auto; + margin: 10px 0; +} + +.message-content pre code { + background-color: transparent; + padding: 0; +} + +.message-content ul, .message-content ol { + margin: 10px 0; + padding-left: 20px; +} + +.message-content li { + margin: 5px 0; +} + +.message.incoming .message-content { + background-color: white; + border: 1px solid #e0e0e0; + color: #333; +} + +.message.outgoing .message-content { + background-color: #7EADDB; + color: white; +} + +.message.outgoing .message-content code { + background-color: rgba(255, 255, 255, 0.2); +} + +.message.outgoing .message-content pre { + background-color: rgba(255, 255, 255, 0.2); +} + +.message-time { + font-size: 11px; + color: #999; + margin-top: 5px; + text-align: right; +} + +.message.incoming .message-time { + text-align: left; +} + +/* Chat Input */ +.chat-input { + padding: 20px; + background-color: white; + border-top: 1px solid #e0e0e0; +} + +.chat-input .input-group { + display: flex; +} + +.chat-input textarea { + flex: 1; + resize: none; + border: 1px solid #ddd; + border-radius: 4px 0 0 4px; + padding: 10px; + font-size: 14px; + min-height: 45px; +} + +.chat-input button { + background-color: #7EADDB; + border: none; + color: white; + padding: 0 25px; + border-radius: 0 4px 4px 0; + font-weight: 600; + transition: background-color 0.3s; +} + +.chat-input button:hover { + background-color: #6589AB; +} + +/* Tool Execution */ +.tool-execution { + background-color: #f0f8ff; + border-left: 3px solid #7EADDB; + padding: 10px; + margin-top: 8px; + border-radius: 4px; + font-family: monospace; + font-size: 12px; +} + +.tool-name { + font-weight: bold; + color: #6589AB; +} + +/* Typing Indicator */ +.typing-indicator { + display: none; + margin-bottom: 20px; +} + +.typing-indicator.active { + display: flex; +} + +.typing-indicator .avatar { + width: 40px; + height: 40px; + border-radius: 50%; + background-color: #7EADDB; + display: flex; + align-items: center; + justify-content: center; + color: white; + font-weight: bold; + flex-shrink: 0; + margin-right: 12px; +} + +.typing-indicator .dots { + background-color: white; + border: 1px solid #e0e0e0; + padding: 12px 20px; + border-radius: 8px; + display: flex; + align-items: center; + gap: 6px; +} + +.typing-indicator .dot { + width: 8px; + height: 8px; + border-radius: 50%; + background-color: #7EADDB; + animation: typing 1.4s infinite; +} + +.typing-indicator .dot:nth-child(2) { + animation-delay: 0.2s; +} + +.typing-indicator .dot:nth-child(3) { + animation-delay: 0.4s; +} + +@keyframes typing { + 0%, 60%, 100% { + transform: translateY(0); + opacity: 0.4; + } + 30% { + transform: translateY(-10px); + opacity: 1; + } +} diff --git a/src/main/resources/public/css/welcome.css b/src/main/resources/public/css/welcome.css new file mode 100644 index 0000000000000000000000000000000000000000..c27aa166186ac0a649cab185f6d78bdcfabee097 --- /dev/null +++ b/src/main/resources/public/css/welcome.css @@ -0,0 +1,110 @@ +/* OpenSlice Chat Assistant - Welcome Page Styles */ + +body { + font-family: 'Open Sans', sans-serif; + margin: 0; + padding: 60px 0 40px 0; + min-height: 100vh; + background-color: white; +} + +/* Navbar */ +.navbar-default { + background-color: rgba(245, 245, 245, 0.93); + border-bottom: 1px solid #e7e7e7; +} + +.navbar-brand img { + height: 25px; +} + +/* Welcome Section */ +.welcome-section { + background: linear-gradient(135deg, #989DC3 0%, #7EADDB 100%); + padding: 80px 0; + text-align: center; + color: #FCFCFF; + margin-top: 0; + min-height: 500px; +} + +.welcome-section h1 { + font-size: 48px; + font-weight: 300; + margin-bottom: 20px; + text-shadow: 2px 2px 4px rgba(0,0,0,0.3); +} + +.welcome-section .subtitle { + font-size: 22px; + margin-bottom: 30px; + font-weight: 300; +} + +.login-message { + background: rgba(255, 255, 255, 0.95); + color: #d9534f; + padding: 15px 30px; + border-radius: 4px; + display: inline-block; + margin-bottom: 30px; + font-weight: 600; + font-size: 16px; +} + +.btn-login { + background-color: #7EADDB; + color: white; + border: none; + padding: 15px 40px; + font-size: 18px; + font-weight: 600; + border-radius: 4px; + transition: all 0.3s; +} + +.btn-login:hover { + background-color: #6589AB; + color: white; + box-shadow: 0 4px 8px rgba(0,0,0,0.2); + transform: translateY(-2px); +} + +/* Features Section */ +.features-section { + background-color: white; + padding: 60px 0; +} + +.features-section h2 { + text-align: center; + font-size: 36px; + font-weight: 300; + margin-bottom: 50px; + color: #333; +} + +.feature-box { + text-align: center; + padding: 30px 20px; + margin-bottom: 30px; +} + +.feature-icon { + font-size: 60px; + margin-bottom: 20px; + color: #7EADDB; +} + +.feature-box h3 { + font-size: 22px; + font-weight: 600; + margin-bottom: 15px; + color: #333; +} + +.feature-box p { + font-size: 15px; + color: #666; + line-height: 1.6; +} diff --git a/src/main/resources/public/images/logo_clear.png b/src/main/resources/public/images/logo_clear.png new file mode 100644 index 0000000000000000000000000000000000000000..2fa5efc135209ce66cee07ea7a76dc0a4cff9244 Binary files /dev/null and b/src/main/resources/public/images/logo_clear.png differ diff --git a/src/main/resources/public/index.html b/src/main/resources/public/index.html index 79a08c2bea2bb2f6475c06925ea501a91d78b9de..688ce8324487c245459a78470c7dfb6a956386b1 100644 --- a/src/main/resources/public/index.html +++ b/src/main/resources/public/index.html @@ -3,65 +3,102 @@ - AI Chat - - - + OpenSlice Chat Assistant + + -
-
-
-
- -
-
- Robot Avatar -
-
-
ChatBot
-

Online

-
-
- - -
- -
-
- Robot Avatar -
-
-
- Hi there! How can I help you today? -
-
10:03 AM
-
-
- -
- - -
-
- - -
-
-
-
-
-
+ + + + + + // Login button handler + $('#loginBtn').click(function() { + window.location.href = '/oauth2/authorization/keycloak'; + }); + + // Logout button handler + $('#logoutBtn').click(function() { + $.ajax({ + type: "POST", + url: "/api/logout", + dataType: 'json' + }) + .done(function(response) { + console.log("Logged out:", response); + accessToken = null; + currentUser = null; + updateAuthUI(); + addMessage("You have been logged out.", false); + }) + .fail(function(jqXHR, textStatus, errorThrown) { + console.error("Logout error:", textStatus, errorThrown); + }); + }); + }); - - - + + + + + + + +
+
+

Welcome to OpenSlice Chat Assistant

+

AI-powered assistance for your OpenSlice platform

+ + + +
+ + +
+
+ + +
+
+

What you can do

+
+
+
+
💬
+

Natural Language Queries

+

Ask questions about your services, catalogs, and deployments using natural language. Get instant answers powered by AI.

+
+
+
+
+
🔧
+

Intelligent Tool Execution

+

Let AI help you interact with TMF APIs and manage resources efficiently. Automate complex workflows with simple commands.

+
+
+
+
+
📊
+

Real-time Information

+

Get instant answers about your platform status, resource utilization, and system health in real-time.

+
+
+
+
+
+ + + + +