feat(chat): added chat interface and connection to ollama #10

Merged
sharang merged 6 commits from feat/CAI-20 into main 2026-02-20 19:40:26 +00:00
6 changed files with 616 additions and 1 deletions
Showing only changes of commit 1f9629f111 - Show all commits

4
.gitignore vendored
View File

@@ -12,9 +12,11 @@
# Logs
*.log
# Keycloak runtime data (but keep realm-export.json)
# Keycloak runtime data (but keep config and theme)
keycloak/*
!keycloak/realm-export.json
!keycloak/themes/
!keycloak/themes/**
# Node modules
node_modules/

View File

@@ -15,6 +15,7 @@ services:
- --import-realm
volumes:
- ./keycloak/realm-export.json:/opt/keycloak/data/import/realm-export.json:ro
- ./keycloak/themes/certifai:/opt/keycloak/themes/certifai:ro
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/health/ready"]
interval: 10s

View File

@@ -9,6 +9,7 @@
"loginWithEmailAllowed": true,
"duplicateEmailsAllowed": false,
"resetPasswordAllowed": true,
"loginTheme": "certifai",
"editUsernameAllowed": false,
"bruteForceProtected": true,
"permanentLockout": false,

View File

@@ -0,0 +1,583 @@
/* CERTifAI Keycloak Login Theme
* Overrides PatternFly v4 / legacy Keycloak classes to match the dashboard.
*
* Actual page structure (Keycloak 26 with parent=keycloak):
* html.login-pf > body
* div.login-pf-page
* div#kc-header.login-pf-page-header
* div#kc-header-wrapper
* div.card-pf
* header.login-pf-header > h1#kc-page-title
* div#kc-content > div#kc-content-wrapper
* form#kc-form-login
* .form-group (email)
* .form-group (password + .pf-c-input-group)
* .form-group.login-pf-settings (forgot pwd)
* .form-group #kc-form-buttons (submit: input#kc-login.pf-c-button.pf-m-primary)
* div#kc-info.login-pf-signup (register link)
*
* Classes used: pf-c-* (PF v4), login-pf-*, card-pf, form-group
*/
/* ===== Google Fonts ===== */
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=Space+Grotesk:wght@500;600;700&display=swap');
/* ===== CSS Variables ===== */
:root {
--cai-bg-body: #0f1116;
--cai-bg-card: #1a1d26;
--cai-bg-surface: #1e222d;
--cai-bg-input: #12141a;
--cai-text-primary: #e2e8f0;
--cai-text-heading: #f1f5f9;
--cai-text-muted: #8892a8;
--cai-text-faint: #5a6478;
--cai-border-primary: #1e222d;
--cai-border-secondary: #2a2f3d;
--cai-accent: #91a4d2;
--cai-accent-secondary: #6d85c6;
--cai-brand-indigo: #4B3FE0;
--cai-brand-teal: #38B2AC;
--cai-error: #f87171;
--cai-success: #4ade80;
}
/* ===== Animations ===== */
/* Slow-moving ambient gradient behind the page */
@keyframes ambientShift {
0% { background-position: 0% 0%; }
25% { background-position: 100% 50%; }
50% { background-position: 50% 100%; }
75% { background-position: 0% 50%; }
100% { background-position: 0% 0%; }
}
/* Subtle glow pulse on the card */
@keyframes cardGlow {
0%, 100% { box-shadow: 0 4px 24px rgba(0, 0, 0, 0.3), 0 0 60px rgba(75, 63, 224, 0.04); }
50% { box-shadow: 0 4px 24px rgba(0, 0, 0, 0.3), 0 0 80px rgba(56, 178, 172, 0.06); }
}
/* Gentle float for the logo */
@keyframes logoFloat {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-4px); }
}
/* Gradient shimmer on the button */
@keyframes buttonShimmer {
0% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
100% { background-position: 0% 50%; }
}
/* ===== Base Page ===== */
html.login-pf {
background-color: var(--cai-bg-body) !important;
}
html.login-pf body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif !important;
background:
radial-gradient(ellipse at 20% 20%, rgba(75, 63, 224, 0.07) 0%, transparent 50%),
radial-gradient(ellipse at 80% 80%, rgba(56, 178, 172, 0.05) 0%, transparent 50%),
radial-gradient(ellipse at 50% 50%, rgba(109, 133, 198, 0.03) 0%, transparent 70%),
var(--cai-bg-body) !important;
background-size: 200% 200%, 200% 200%, 100% 100%, 100% 100% !important;
animation: ambientShift 20s ease-in-out infinite !important;
color: var(--cai-text-primary) !important;
min-height: 100vh;
}
/* ===== Page Layout ===== */
.login-pf-page {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 100vh;
padding: 40px 24px;
position: relative;
}
/* ===== Header (Logo + Realm Name) ===== */
#kc-header.login-pf-page-header {
background: transparent !important;
background-image: none !important;
padding: 0 0 32px !important;
text-align: center;
max-width: 440px;
width: 100%;
margin: 0;
}
#kc-header-wrapper {
font-family: 'Space Grotesk', sans-serif !important;
font-size: 28px !important;
font-weight: 700 !important;
color: var(--cai-text-heading) !important;
letter-spacing: -0.02em;
text-transform: none !important;
padding: 0 !important;
}
/* Logo via ::before pseudo-element */
#kc-header-wrapper::before {
content: '';
display: block;
width: 64px;
height: 64px;
margin: 0 auto 16px;
background-image: url('../img/logo.svg');
background-size: contain;
background-repeat: no-repeat;
background-position: center;
animation: logoFloat 4s ease-in-out infinite;
filter: drop-shadow(0 0 12px rgba(75, 63, 224, 0.3));
}
/* ===== Login Card ===== */
.card-pf {
background-color: var(--cai-bg-card) !important;
border: 1px solid var(--cai-border-secondary) !important;
border-radius: 12px !important;
max-width: 440px;
width: 100%;
padding: 32px !important;
margin: 0 !important;
animation: cardGlow 6s ease-in-out infinite;
position: relative;
overflow: hidden;
}
/* Subtle gradient border effect on the card via ::before overlay */
.card-pf::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 2px;
background: linear-gradient(
90deg,
transparent,
var(--cai-brand-indigo),
var(--cai-brand-teal),
var(--cai-accent-secondary),
transparent
);
opacity: 0.5;
}
/* ===== Card Header (Sign In Title) ===== */
.login-pf-header {
border-bottom: none !important;
padding: 0 0 24px !important;
margin: 0 !important;
}
#kc-page-title {
font-family: 'Space Grotesk', sans-serif !important;
font-size: 22px !important;
font-weight: 600 !important;
color: var(--cai-text-heading) !important;
text-align: center;
margin: 0 !important;
}
/* ===== Form Groups ===== */
.form-group {
margin-bottom: 20px !important;
}
/* ===== Labels ===== */
.pf-c-form__label,
.pf-c-form__label-text,
.login-pf-page .form-group label,
.card-pf label {
font-family: 'Inter', sans-serif !important;
font-size: 13px !important;
font-weight: 500 !important;
color: var(--cai-text-muted) !important;
margin-bottom: 6px !important;
display: block;
}
/* ===== Text Inputs ===== */
.pf-c-form-control,
.login-pf-page .form-control,
.card-pf input[type="text"],
.card-pf input[type="password"],
.card-pf input[type="email"] {
background-color: var(--cai-bg-input) !important;
border: 1px solid var(--cai-border-secondary) !important;
border-radius: 8px !important;
color: var(--cai-text-primary) !important;
font-family: 'Inter', sans-serif !important;
font-size: 14px !important;
padding: 10px 14px !important;
height: auto !important;
line-height: 1.5 !important;
transition: border-color 0.2s ease, box-shadow 0.2s ease !important;
box-shadow: none !important;
outline: none !important;
}
.pf-c-form-control:focus,
.pf-c-form-control:focus-within,
.card-pf input[type="text"]:focus,
.card-pf input[type="password"]:focus,
.card-pf input[type="email"]:focus {
border-color: var(--cai-accent) !important;
box-shadow: 0 0 0 1px var(--cai-accent), 0 0 12px rgba(145, 164, 210, 0.1) !important;
outline: none !important;
}
.pf-c-form-control::placeholder,
.card-pf input::placeholder {
color: var(--cai-text-faint) !important;
}
/* Override browser autofill yellow background */
input:-webkit-autofill,
input:-webkit-autofill:hover,
input:-webkit-autofill:focus,
input:-webkit-autofill:active {
-webkit-box-shadow: 0 0 0 9999px var(--cai-bg-input) inset !important;
-webkit-text-fill-color: var(--cai-text-primary) !important;
caret-color: var(--cai-text-primary) !important;
transition: background-color 5000s ease-in-out 0s !important;
background-color: var(--cai-bg-input) !important;
color: var(--cai-text-primary) !important;
}
/* Firefox autofill override */
input:autofill {
background-color: var(--cai-bg-input) !important;
color: var(--cai-text-primary) !important;
border-color: var(--cai-border-secondary) !important;
}
/* Additional specificity for autofill inside input-group */
.pf-c-input-group input:-webkit-autofill,
.card-pf input:-webkit-autofill,
.form-group input:-webkit-autofill,
#username:-webkit-autofill,
#password:-webkit-autofill {
-webkit-box-shadow: 0 0 0 9999px var(--cai-bg-input) inset !important;
-webkit-text-fill-color: var(--cai-text-primary) !important;
background-color: var(--cai-bg-input) !important;
}
/* ===== Password Input Group ===== */
/* FIX: The .pf-c-input-group has white bg from PF4, causing white corners
* behind the rounded child elements. Set transparent + matching border-radius. */
.pf-c-input-group {
display: flex !important;
align-items: stretch !important;
background-color: transparent !important;
background: transparent !important;
border-radius: 8px !important;
overflow: hidden !important;
}
.pf-c-input-group > .pf-c-form-control,
.pf-c-input-group > input.pf-c-form-control,
.pf-c-input-group > input[type="password"],
#password {
border-radius: 8px 0 0 8px !important;
border-right: none !important;
flex: 1;
}
/* Password visibility toggle */
.pf-c-button.pf-m-control,
.pf-c-input-group > .pf-c-button.pf-m-control {
background-color: var(--cai-bg-surface) !important;
color: var(--cai-text-muted) !important;
border-top: 1px solid var(--cai-border-secondary) !important;
border-right: 1px solid var(--cai-border-secondary) !important;
border-bottom: 1px solid var(--cai-border-secondary) !important;
border-left: 1px solid var(--cai-border-primary) !important;
border-radius: 0 8px 8px 0 !important;
padding: 0 14px !important;
transition: color 0.2s ease, background-color 0.2s ease !important;
line-height: 1 !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
}
.pf-c-button.pf-m-control:hover,
.pf-c-input-group > .pf-c-button.pf-m-control:hover {
color: var(--cai-accent) !important;
background-color: rgba(145, 164, 210, 0.08) !important;
}
.pf-c-button.pf-m-control:focus,
.pf-c-input-group > .pf-c-button.pf-m-control:focus {
box-shadow: none !important;
outline: none !important;
}
/* ===== Primary Button (Sign In) ===== */
.pf-c-button.pf-m-primary,
input.pf-c-button.pf-m-primary,
#kc-login {
background: linear-gradient(135deg,
var(--cai-accent),
var(--cai-accent-secondary),
var(--cai-brand-indigo),
var(--cai-accent-secondary),
var(--cai-accent)) !important;
background-size: 300% 100% !important;
animation: buttonShimmer 6s ease-in-out infinite !important;
border: none !important;
border-radius: 8px !important;
color: #0a0c10 !important;
font-family: 'Inter', sans-serif !important;
font-size: 14px !important;
font-weight: 600 !important;
padding: 12px 20px !important;
cursor: pointer !important;
transition: opacity 0.15s ease, box-shadow 0.2s ease !important;
text-shadow: none !important;
box-shadow: 0 2px 12px rgba(109, 133, 198, 0.2) !important;
width: 100%;
text-align: center;
}
.pf-c-button.pf-m-primary:hover,
input.pf-c-button.pf-m-primary:hover,
#kc-login:hover {
opacity: 0.95;
box-shadow: 0 4px 20px rgba(109, 133, 198, 0.35) !important;
}
.pf-c-button.pf-m-primary:focus,
#kc-login:focus {
box-shadow: 0 0 0 2px var(--cai-accent), 0 4px 20px rgba(109, 133, 198, 0.3) !important;
outline: none !important;
}
/* ===== Links ===== */
.login-pf-page a,
.card-pf a {
color: var(--cai-accent) !important;
text-decoration: none !important;
transition: color 0.15s ease !important;
}
.login-pf-page a:hover,
.card-pf a:hover {
color: var(--cai-accent-secondary) !important;
text-decoration: none !important;
}
/* Forgot Password link */
.login-pf-settings {
text-align: right;
margin-bottom: 24px !important;
}
.login-pf-settings a {
font-size: 13px !important;
}
/* ===== Registration / Info Section ===== */
#kc-info.login-pf-signup {
background-color: var(--cai-bg-surface) !important;
border-top: 1px solid var(--cai-border-primary) !important;
padding: 16px 32px !important;
margin: 0 -32px -32px !important;
border-radius: 0 0 12px 12px !important;
text-align: center;
}
#kc-info-wrapper,
#kc-registration {
font-size: 14px !important;
color: var(--cai-text-muted) !important;
}
#kc-registration span {
color: var(--cai-text-muted) !important;
}
/* ===== Alert / Error Messages ===== */
.alert,
.pf-c-alert {
background-color: var(--cai-bg-surface) !important;
border: 1px solid var(--cai-border-secondary) !important;
border-radius: 8px !important;
color: var(--cai-text-primary) !important;
padding: 12px 16px !important;
margin-bottom: 16px !important;
font-size: 14px !important;
}
.alert-error,
.alert-warning,
.pf-c-alert.pf-m-danger,
.pf-c-alert.pf-m-warning {
border-color: var(--cai-error) !important;
}
.alert-error .kc-feedback-text,
.pf-c-alert .pf-c-alert__title {
color: var(--cai-text-primary) !important;
}
.alert-success {
border-color: var(--cai-success) !important;
}
/* ===== Checkboxes (Remember Me) ===== */
.pf-c-check,
.login-pf-page .checkbox {
display: flex;
align-items: center;
gap: 8px;
}
.pf-c-check__label,
.login-pf-page .checkbox label {
font-size: 13px !important;
color: var(--cai-text-muted) !important;
cursor: pointer;
}
.pf-c-check__input,
.login-pf-page input[type="checkbox"] {
accent-color: var(--cai-accent);
width: 16px;
height: 16px;
}
/* ===== Select / Dropdown ===== */
.card-pf select,
.login-pf-page select {
background-color: var(--cai-bg-input) !important;
border: 1px solid var(--cai-border-secondary) !important;
border-radius: 8px !important;
color: var(--cai-text-primary) !important;
padding: 10px 14px !important;
font-family: 'Inter', sans-serif !important;
font-size: 14px !important;
}
/* ===== Social Login / Identity Providers ===== */
#kc-social-providers {
margin-top: 20px;
padding-top: 20px;
border-top: 1px solid var(--cai-border-primary);
}
#kc-social-providers ul {
list-style: none;
padding: 0;
margin: 0;
}
#kc-social-providers li {
margin-bottom: 8px;
}
#kc-social-providers a,
#kc-social-providers .pf-c-button {
background-color: var(--cai-bg-surface) !important;
border: 1px solid var(--cai-border-secondary) !important;
border-radius: 8px !important;
color: var(--cai-text-primary) !important;
padding: 10px 16px !important;
display: block;
text-align: center;
font-size: 14px !important;
font-weight: 500 !important;
transition: border-color 0.15s ease !important;
}
#kc-social-providers a:hover,
#kc-social-providers .pf-c-button:hover {
border-color: var(--cai-accent) !important;
}
/* ===== Form Buttons Row ===== */
#kc-form-buttons {
margin-top: 8px !important;
}
#kc-form-options {
margin-bottom: 4px;
}
/* ===== Tooltip ===== */
.kc-tooltip-text {
background-color: var(--cai-bg-surface) !important;
color: var(--cai-text-primary) !important;
border: 1px solid var(--cai-border-secondary) !important;
border-radius: 8px !important;
font-size: 13px !important;
}
/* ===== Scrollbar ===== */
::-webkit-scrollbar {
width: 6px;
}
::-webkit-scrollbar-track {
background: var(--cai-bg-body);
}
::-webkit-scrollbar-thumb {
background: var(--cai-border-secondary);
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--cai-text-faint);
}
/* ===== Responsive ===== */
@media (max-width: 768px) {
.login-pf-page {
padding: 24px 16px;
}
.card-pf {
padding: 24px !important;
}
#kc-header-wrapper {
font-size: 24px !important;
}
#kc-header-wrapper::before {
width: 48px;
height: 48px;
}
#kc-info.login-pf-signup {
margin: 0 -24px -24px !important;
padding: 16px 24px !important;
}
}
/* ===== Override PatternFly background images ===== */
.login-pf-page .login-pf-page-header,
.login-pf body {
background-image: none !important;
}
/* Remove any PF4 container-fluid stretching */
.container-fluid {
padding: 0 !important;
max-width: none !important;
}
/* Ensure the card doesn't stretch full width */
.login-pf-page > .card-pf {
max-width: 440px;
margin: 0 auto !important;
}

View File

@@ -0,0 +1,25 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="none">
<!-- Shield body -->
<path d="M32 4L8 16v16c0 14.4 10.24 27.2 24 32 13.76-4.8 24-17.6 24-32V16L32 4z"
fill="#4B3FE0" fill-opacity="0.12" stroke="#4B3FE0" stroke-width="2"
stroke-linejoin="round"/>
<!-- Inner shield highlight -->
<path d="M32 10L14 19v11c0 11.6 7.68 22 18 26 10.32-4 18-14.4 18-26V19L32 10z"
fill="none" stroke="#4B3FE0" stroke-width="1" stroke-opacity="0.3"
stroke-linejoin="round"/>
<!-- Neural network nodes -->
<circle cx="32" cy="24" r="3.5" fill="#38B2AC"/>
<circle cx="22" cy="36" r="3" fill="#38B2AC"/>
<circle cx="42" cy="36" r="3" fill="#38B2AC"/>
<circle cx="27" cy="48" r="2.5" fill="#38B2AC" fill-opacity="0.7"/>
<circle cx="37" cy="48" r="2.5" fill="#38B2AC" fill-opacity="0.7"/>
<!-- Neural network edges -->
<line x1="32" y1="24" x2="22" y2="36" stroke="#38B2AC" stroke-width="1.2" stroke-opacity="0.6"/>
<line x1="32" y1="24" x2="42" y2="36" stroke="#38B2AC" stroke-width="1.2" stroke-opacity="0.6"/>
<line x1="22" y1="36" x2="27" y2="48" stroke="#38B2AC" stroke-width="1" stroke-opacity="0.4"/>
<line x1="22" y1="36" x2="37" y2="48" stroke="#38B2AC" stroke-width="1" stroke-opacity="0.4"/>
<line x1="42" y1="36" x2="27" y2="48" stroke="#38B2AC" stroke-width="1" stroke-opacity="0.4"/>
<line x1="42" y1="36" x2="37" y2="48" stroke="#38B2AC" stroke-width="1" stroke-opacity="0.4"/>
<!-- Cross edge for connectivity -->
<line x1="22" y1="36" x2="42" y2="36" stroke="#38B2AC" stroke-width="0.8" stroke-opacity="0.3"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -0,0 +1,3 @@
parent=keycloak
import=common/keycloak
styles=css/login.css