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:
chihlasm
2026-03-04 20:44:25 -05:00
committed by GitHub
parent 554ad84d9e
commit 4d2c4930fd
100 changed files with 8181 additions and 466 deletions

View 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>
)
}

View 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