feat: Slate & Ice Modern aesthetic redesign (#94)
* chore: update Google Fonts to Bricolage Grotesque, IBM Plex Sans, JetBrains Mono Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * chore: update Tailwind config to Slate & Ice theme colors and fonts Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: update CSS variables and glass-card utilities for Slate & Ice theme - Replace all color variables with Slate & Ice palette - Add glass system vars (--glass-bg, --glass-blur, --shadow-float) - Replace legacy glass-card with new variable-driven glass classes - Add breatheGlow, bellWobble, slideDown, fadeInRight keyframes - Update font references to IBM Plex Sans and Bricolage Grotesque Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: recolor BrandLogo to cyan gradient, split BrandWordmark for gradient Flow text Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: update TopBar with glassmorphism backdrop and cyan accent styling Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: update Sidebar with glassmorphism backdrop Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: add ambient atmosphere gradient orbs behind app shell Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: update QuickStats and SessionsPanel with glass-card styling Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: add WeeklyCalendar, QuickActions, OpenSessions, RecentActivity dashboard components Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: redesign dashboard layout with calendar, open sessions, and glass-card panels New layout: greeting → calendar+actions → sessions+stats → activity Replaces old QuickStats and SessionsPanel with new dashboard components Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: replace remaining purple hex references with ice-cyan accent Sweep of hardcoded purple hex values (#818cf8, #6366f1) replaced with new cyan accent (#06b6d4) in QuickActions, RecentActivity, QuickLaunch, and SVG brand assets. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * docs: update CLAUDE.md branding and design system for Slate & Ice Modern Updated Last Updated date, branding section (fonts, colors, glass utilities, atmosphere orbs), component styling rules, and Design System section to reflect the new ice-cyan glassmorphism theme. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * docs: add Slate & Ice Modern design doc and implementation plan Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: redesign login page with Slate & Ice Modern design system Apply glassmorphism styling, atmosphere orbs, branded wordmark, and consistent design tokens to match the updated app shell aesthetic. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: raise TopBar z-index so profile dropdown renders above main content Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: add AI assistant with in-session copilot and standalone chat with RAG Implements three-phase AI assistant feature: - Phase 0: RAG infrastructure with pgvector embeddings, Voyage AI integration, tree chunking service, and semantic search over team's flow library - Phase 1: In-session copilot panel during flow navigation with contextual AI help, current step awareness, and suggested related flows - Phase 2: Standalone AI chat page with persistent conversation history, pin/delete, and configurable retention policies (account-level) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: add account management, email verification, AI fixes, and user guides - Profile settings, account transfer, delete/leave account flows - Email verification with JWT tokens and Resend integration - AI assistant/copilot fixes: markdown rendering, shared RAG helpers, token tracking, input refocus, model_validate usage - User guides hub + detail pages with 13 topic guides - Sidebar and top bar navigation for guides Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: prevent stale chunk errors after deployments - Set Cache-Control no-cache on index.html in nginx so browsers always fetch fresh chunk references after a deploy - Auto-reload on chunk load failures (stale deploy detection) with loop prevention via sessionStorage - Show friendly "App Updated" message if auto-reload doesn't resolve it Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: add email verification toggle to admin settings Adds platform-level toggle to enable/disable email verification. When disabled, the verification banner is hidden and the send endpoint returns 403. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit was merged in pull request #94.
This commit is contained in:
119
frontend/src/pages/account/ChatRetentionSettingsPage.tsx
Normal file
119
frontend/src/pages/account/ChatRetentionSettingsPage.tsx
Normal file
@@ -0,0 +1,119 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { Save, Loader2, Clock } from 'lucide-react'
|
||||
import { assistantChatApi } from '@/api/assistantChat'
|
||||
|
||||
export default function ChatRetentionSettingsPage() {
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [saving, setSaving] = useState(false)
|
||||
const [retentionDays, setRetentionDays] = useState('')
|
||||
const [maxCount, setMaxCount] = useState('')
|
||||
const [success, setSuccess] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
loadSettings()
|
||||
}, [])
|
||||
|
||||
const loadSettings = async () => {
|
||||
try {
|
||||
const data = await assistantChatApi.getRetentionSettings()
|
||||
setRetentionDays(data.chat_retention_days?.toString() ?? '90')
|
||||
setMaxCount(data.chat_retention_max_count?.toString() ?? '100')
|
||||
} catch {
|
||||
// silently handle
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSave = async () => {
|
||||
setSaving(true)
|
||||
setSuccess(false)
|
||||
try {
|
||||
await assistantChatApi.updateRetentionSettings({
|
||||
chat_retention_days: parseInt(retentionDays) || null,
|
||||
chat_retention_max_count: parseInt(maxCount) || null,
|
||||
})
|
||||
setSuccess(true)
|
||||
setTimeout(() => setSuccess(false), 3000)
|
||||
} catch {
|
||||
// silently handle
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center py-20">
|
||||
<Loader2 className="animate-spin text-primary" size={24} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="max-w-2xl mx-auto py-8 px-6">
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<Clock size={20} className="text-primary" />
|
||||
<h1 className="text-xl font-heading font-bold text-foreground">Chat Retention</h1>
|
||||
</div>
|
||||
|
||||
<div className="glass-card-static rounded-2xl p-6 space-y-6">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Configure how long AI assistant conversations are retained. Pinned chats are never automatically deleted.
|
||||
</p>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="font-label text-[0.625rem] uppercase tracking-[0.1em] text-muted-foreground block mb-1.5">
|
||||
Retention Period (days)
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={retentionDays}
|
||||
onChange={e => setRetentionDays(e.target.value)}
|
||||
min={1}
|
||||
max={365}
|
||||
className="w-full rounded-xl border bg-card text-foreground text-sm px-4 py-2.5 focus:outline-none focus:border-[rgba(6,182,212,0.3)]"
|
||||
style={{ borderColor: 'var(--glass-border)' }}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
Chats older than this will be automatically deleted (1-365 days)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="font-label text-[0.625rem] uppercase tracking-[0.1em] text-muted-foreground block mb-1.5">
|
||||
Max Conversations
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={maxCount}
|
||||
onChange={e => setMaxCount(e.target.value)}
|
||||
min={10}
|
||||
max={10000}
|
||||
className="w-full rounded-xl border bg-card text-foreground text-sm px-4 py-2.5 focus:outline-none focus:border-[rgba(6,182,212,0.3)]"
|
||||
style={{ borderColor: 'var(--glass-border)' }}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
When this limit is exceeded, oldest unpinned chats are deleted
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3 pt-2">
|
||||
<button
|
||||
onClick={handleSave}
|
||||
disabled={saving}
|
||||
className="bg-gradient-brand text-[#101114] font-semibold text-sm rounded-[10px] px-5 py-2.5 hover:opacity-90 active:scale-[0.97] transition-all disabled:opacity-40 flex items-center gap-2"
|
||||
>
|
||||
{saving ? <Loader2 size={14} className="animate-spin" /> : <Save size={14} />}
|
||||
Save Settings
|
||||
</button>
|
||||
{success && (
|
||||
<span className="text-sm text-emerald-400">Settings saved</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
184
frontend/src/pages/account/ProfileSettingsPage.tsx
Normal file
184
frontend/src/pages/account/ProfileSettingsPage.tsx
Normal file
@@ -0,0 +1,184 @@
|
||||
import { useState } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { User as UserIcon, Loader2, AlertCircle, Check } from 'lucide-react'
|
||||
import { authApi } from '@/api/auth'
|
||||
import { useAuthStore } from '@/store/authStore'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { toast } from '@/lib/toast'
|
||||
import type { UserUpdate } from '@/types'
|
||||
|
||||
const inputClass = cn(
|
||||
'mt-1 block w-full rounded-[10px] border border-border bg-card px-3 py-2',
|
||||
'text-foreground placeholder:text-muted-foreground',
|
||||
'focus:border-[rgba(6,182,212,0.3)] focus:outline-none focus:ring-1 focus:ring-primary/20'
|
||||
)
|
||||
|
||||
export function ProfileSettingsPage() {
|
||||
const user = useAuthStore((s) => s.user)
|
||||
const fetchUser = useAuthStore((s) => s.fetchUser)
|
||||
|
||||
const [name, setName] = useState(user?.name ?? '')
|
||||
const [email, setEmail] = useState(user?.email ?? '')
|
||||
const [phone, setPhone] = useState(user?.phone ?? '')
|
||||
const [jobTitle, setJobTitle] = useState(user?.job_title ?? '')
|
||||
const [timezone, setTimezone] = useState(user?.timezone ?? 'UTC')
|
||||
const [currentPassword, setCurrentPassword] = useState('')
|
||||
const [isSaving, setIsSaving] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
const emailChanged = email !== user?.email
|
||||
const hasChanges =
|
||||
emailChanged ||
|
||||
name !== user?.name ||
|
||||
phone !== (user?.phone ?? '') ||
|
||||
jobTitle !== (user?.job_title ?? '') ||
|
||||
timezone !== (user?.timezone ?? 'UTC')
|
||||
|
||||
const handleSave = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
if (!hasChanges) return
|
||||
|
||||
setIsSaving(true)
|
||||
setError(null)
|
||||
try {
|
||||
const payload: UserUpdate = {}
|
||||
if (name !== user?.name) payload.name = name.trim()
|
||||
if (emailChanged) {
|
||||
payload.email = email.trim()
|
||||
payload.current_password = currentPassword
|
||||
}
|
||||
if (phone !== (user?.phone ?? '')) payload.phone = phone.trim() || null
|
||||
if (jobTitle !== (user?.job_title ?? '')) payload.job_title = jobTitle.trim() || null
|
||||
if (timezone !== (user?.timezone ?? 'UTC')) payload.timezone = timezone
|
||||
|
||||
await authApi.updateProfile(payload)
|
||||
await fetchUser()
|
||||
setCurrentPassword('')
|
||||
toast.success('Profile updated')
|
||||
} catch (err) {
|
||||
const axiosErr = err as { response?: { data?: { detail?: string } } }
|
||||
setError(axiosErr.response?.data?.detail ?? 'Failed to update profile')
|
||||
} finally {
|
||||
setIsSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-8">
|
||||
<div className="flex items-center gap-3">
|
||||
<UserIcon className="h-8 w-8 text-muted-foreground" />
|
||||
<h1 className="text-2xl font-bold font-heading text-foreground sm:text-3xl">Profile Settings</h1>
|
||||
</div>
|
||||
<p className="mt-2 text-muted-foreground">
|
||||
Update your name, email, and personal details
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="max-w-xl">
|
||||
<form onSubmit={handleSave} className="glass-card-static p-6 space-y-5">
|
||||
{/* Name */}
|
||||
<div>
|
||||
<label htmlFor="profile-name" className="block text-sm font-medium text-foreground">Name</label>
|
||||
<input id="profile-name" type="text" value={name} onChange={(e) => setName(e.target.value)} required className={inputClass} />
|
||||
</div>
|
||||
|
||||
{/* Email */}
|
||||
<div>
|
||||
<label htmlFor="profile-email" className="block text-sm font-medium text-foreground">Email</label>
|
||||
<input id="profile-email" type="email" value={email} onChange={(e) => setEmail(e.target.value)} required className={inputClass} />
|
||||
</div>
|
||||
|
||||
{/* Password confirmation for email change */}
|
||||
{emailChanged && (
|
||||
<div>
|
||||
<label htmlFor="profile-password" className="block text-sm font-medium text-foreground">Current Password</label>
|
||||
<p className="text-xs text-muted-foreground">Required to change your email address</p>
|
||||
<input id="profile-password" type="password" value={currentPassword} onChange={(e) => setCurrentPassword(e.target.value)} required className={inputClass} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Phone */}
|
||||
<div>
|
||||
<label htmlFor="profile-phone" className="block text-sm font-medium text-foreground">Phone</label>
|
||||
<input id="profile-phone" type="tel" value={phone} onChange={(e) => setPhone(e.target.value)} placeholder="Optional" className={inputClass} />
|
||||
</div>
|
||||
|
||||
{/* Job Title */}
|
||||
<div>
|
||||
<label htmlFor="profile-job-title" className="block text-sm font-medium text-foreground">Job Title</label>
|
||||
<input id="profile-job-title" type="text" value={jobTitle} onChange={(e) => setJobTitle(e.target.value)} placeholder="e.g. Network Engineer" className={inputClass} />
|
||||
</div>
|
||||
|
||||
{/* Timezone */}
|
||||
<div>
|
||||
<label htmlFor="profile-timezone" className="block text-sm font-medium text-foreground">Timezone</label>
|
||||
<select id="profile-timezone" value={timezone} onChange={(e) => setTimezone(e.target.value)} className={inputClass}>
|
||||
{COMMON_TIMEZONES.map((tz) => (
|
||||
<option key={tz} value={tz}>{tz}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="flex items-center gap-2 text-sm text-rose-500">
|
||||
<AlertCircle className="h-4 w-4 flex-shrink-0" />
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-3 pt-2">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isSaving || !hasChanges}
|
||||
className={cn(
|
||||
'inline-flex items-center gap-2 rounded-[10px] bg-gradient-brand px-4 py-2 text-sm font-semibold text-[#101114]',
|
||||
'shadow-lg shadow-primary/20 hover:opacity-90 active:scale-[0.97]',
|
||||
'disabled:opacity-50 disabled:cursor-not-allowed'
|
||||
)}
|
||||
>
|
||||
{isSaving ? <Loader2 className="h-4 w-4 animate-spin" /> : <Check className="h-4 w-4" />}
|
||||
Save Changes
|
||||
</button>
|
||||
|
||||
<Link
|
||||
to="/change-password"
|
||||
className={cn(
|
||||
'inline-flex items-center rounded-[10px] px-4 py-2 text-sm font-medium',
|
||||
'bg-[rgba(255,255,255,0.04)] border border-[rgba(255,255,255,0.06)] text-foreground',
|
||||
'hover:border-[rgba(255,255,255,0.12)]'
|
||||
)}
|
||||
>
|
||||
Change Password
|
||||
</Link>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const COMMON_TIMEZONES = [
|
||||
'UTC',
|
||||
'America/New_York',
|
||||
'America/Chicago',
|
||||
'America/Denver',
|
||||
'America/Los_Angeles',
|
||||
'America/Anchorage',
|
||||
'Pacific/Honolulu',
|
||||
'America/Toronto',
|
||||
'America/Vancouver',
|
||||
'Europe/London',
|
||||
'Europe/Paris',
|
||||
'Europe/Berlin',
|
||||
'Europe/Amsterdam',
|
||||
'Asia/Tokyo',
|
||||
'Asia/Shanghai',
|
||||
'Asia/Kolkata',
|
||||
'Asia/Dubai',
|
||||
'Australia/Sydney',
|
||||
'Australia/Melbourne',
|
||||
'Pacific/Auckland',
|
||||
]
|
||||
|
||||
export default ProfileSettingsPage
|
||||
Reference in New Issue
Block a user