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:
230
frontend/src/pages/AssistantChatPage.tsx
Normal file
230
frontend/src/pages/AssistantChatPage.tsx
Normal file
@@ -0,0 +1,230 @@
|
||||
import { useState, useEffect, useRef, useCallback } from 'react'
|
||||
import { Sparkles, Send, Loader2 } from 'lucide-react'
|
||||
import { assistantChatApi } from '@/api/assistantChat'
|
||||
import { toast } from '@/lib/toast'
|
||||
import { ChatSidebar } from '@/components/assistant/ChatSidebar'
|
||||
import { ChatMessage } from '@/components/assistant/ChatMessage'
|
||||
import type { ChatListItem, AssistantChatMessage as ChatMessageType } from '@/types/assistant-chat'
|
||||
import type { SuggestedFlow } from '@/types/copilot'
|
||||
|
||||
interface MessageWithMeta extends ChatMessageType {
|
||||
suggestedFlows?: SuggestedFlow[]
|
||||
}
|
||||
|
||||
export default function AssistantChatPage() {
|
||||
const [chats, setChats] = useState<ChatListItem[]>([])
|
||||
const [activeChatId, setActiveChatId] = useState<string | null>(null)
|
||||
const [messages, setMessages] = useState<MessageWithMeta[]>([])
|
||||
const [input, setInput] = useState('')
|
||||
const [loading, setLoading] = useState(false)
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null)
|
||||
const inputRef = useRef<HTMLTextAreaElement>(null)
|
||||
|
||||
// Load chat list
|
||||
useEffect(() => {
|
||||
loadChats()
|
||||
}, [])
|
||||
|
||||
// Auto-scroll
|
||||
useEffect(() => {
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
|
||||
}, [messages])
|
||||
|
||||
const loadChats = async () => {
|
||||
try {
|
||||
const list = await assistantChatApi.listChats(1, 100)
|
||||
setChats(list)
|
||||
} catch {
|
||||
// silently handle
|
||||
}
|
||||
}
|
||||
|
||||
const selectChat = useCallback(async (chatId: string) => {
|
||||
setActiveChatId(chatId)
|
||||
try {
|
||||
const chat = await assistantChatApi.getChat(chatId)
|
||||
setMessages(chat.messages.map(m => ({ ...m })))
|
||||
} catch {
|
||||
setMessages([])
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleNewChat = async () => {
|
||||
try {
|
||||
const chat = await assistantChatApi.createChat()
|
||||
setChats(prev => [
|
||||
{ id: chat.id, title: chat.title, message_count: 0, pinned: false, created_at: chat.created_at, updated_at: chat.updated_at },
|
||||
...prev,
|
||||
])
|
||||
setActiveChatId(chat.id)
|
||||
setMessages([])
|
||||
} catch {
|
||||
toast.error('Failed to create chat')
|
||||
}
|
||||
}
|
||||
|
||||
const handleDeleteChat = async (chatId: string) => {
|
||||
try {
|
||||
await assistantChatApi.deleteChat(chatId)
|
||||
setChats(prev => prev.filter(c => c.id !== chatId))
|
||||
if (activeChatId === chatId) {
|
||||
setActiveChatId(null)
|
||||
setMessages([])
|
||||
}
|
||||
} catch {
|
||||
toast.error('Failed to delete chat')
|
||||
}
|
||||
}
|
||||
|
||||
const handleTogglePin = async (chatId: string, pinned: boolean) => {
|
||||
try {
|
||||
await assistantChatApi.updateChat(chatId, { pinned })
|
||||
setChats(prev =>
|
||||
prev.map(c => c.id === chatId ? { ...c, pinned } : c)
|
||||
)
|
||||
} catch {
|
||||
toast.error('Failed to update chat')
|
||||
}
|
||||
}
|
||||
|
||||
const handleSend = async () => {
|
||||
if (!input.trim() || !activeChatId || loading) return
|
||||
|
||||
const userMessage = input.trim()
|
||||
setInput('')
|
||||
setMessages(prev => [...prev, { role: 'user', content: userMessage }])
|
||||
setLoading(true)
|
||||
|
||||
try {
|
||||
const response = await assistantChatApi.sendMessage(activeChatId, userMessage)
|
||||
setMessages(prev => [
|
||||
...prev,
|
||||
{ role: 'assistant', content: response.content, suggestedFlows: response.suggested_flows },
|
||||
])
|
||||
// Update chat list title if it was the first message
|
||||
setChats(prev =>
|
||||
prev.map(c =>
|
||||
c.id === activeChatId
|
||||
? { ...c, message_count: c.message_count + 2, title: c.message_count === 0 ? userMessage.slice(0, 100) : c.title, updated_at: new Date().toISOString() }
|
||||
: c
|
||||
)
|
||||
)
|
||||
} catch {
|
||||
setMessages(prev => [
|
||||
...prev,
|
||||
{ role: 'assistant', content: 'Sorry, something went wrong. Please try again.' },
|
||||
])
|
||||
} finally {
|
||||
setLoading(false)
|
||||
requestAnimationFrame(() => inputRef.current?.focus())
|
||||
}
|
||||
}
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault()
|
||||
handleSend()
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-[calc(100vh-3.5rem)]">
|
||||
{/* Sidebar */}
|
||||
<ChatSidebar
|
||||
chats={chats}
|
||||
activeChatId={activeChatId}
|
||||
onSelectChat={selectChat}
|
||||
onNewChat={handleNewChat}
|
||||
onDeleteChat={handleDeleteChat}
|
||||
onTogglePin={handleTogglePin}
|
||||
/>
|
||||
|
||||
{/* Main chat area */}
|
||||
<div className="flex-1 flex flex-col min-w-0">
|
||||
{activeChatId ? (
|
||||
<>
|
||||
{/* Messages */}
|
||||
<div className="flex-1 overflow-y-auto px-6 py-4 space-y-4">
|
||||
{messages.length === 0 && !loading && (
|
||||
<div className="flex flex-col items-center justify-center h-full text-center">
|
||||
<div className="w-16 h-16 rounded-full bg-primary/10 flex items-center justify-center mb-4">
|
||||
<Sparkles size={28} className="text-primary" />
|
||||
</div>
|
||||
<h2 className="text-lg font-heading font-semibold text-foreground mb-2">
|
||||
AI Assistant
|
||||
</h2>
|
||||
<p className="text-sm text-muted-foreground max-w-md">
|
||||
Ask me anything about IT infrastructure, networking, Active Directory,
|
||||
cloud platforms, or troubleshooting. I'll also suggest relevant flows from your team's library.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{messages.map((msg, i) => (
|
||||
<ChatMessage
|
||||
key={i}
|
||||
role={msg.role}
|
||||
content={msg.content}
|
||||
suggestedFlows={msg.suggestedFlows}
|
||||
/>
|
||||
))}
|
||||
{loading && (
|
||||
<div className="flex gap-3">
|
||||
<div className="w-8 h-8 rounded-full bg-primary/15 flex items-center justify-center">
|
||||
<Sparkles size={14} className="text-primary" />
|
||||
</div>
|
||||
<div className="bg-[rgba(255,255,255,0.04)] border border-[rgba(255,255,255,0.06)] rounded-2xl px-4 py-3">
|
||||
<Loader2 size={16} className="animate-spin text-primary" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
|
||||
{/* Input */}
|
||||
<div className="px-6 py-4 border-t shrink-0" style={{ borderColor: 'var(--glass-border)' }}>
|
||||
<div className="flex items-end gap-3 max-w-3xl mx-auto">
|
||||
<textarea
|
||||
ref={inputRef}
|
||||
value={input}
|
||||
onChange={e => setInput(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder="Ask about IT, networking, troubleshooting..."
|
||||
rows={1}
|
||||
className="flex-1 resize-none rounded-xl border bg-card text-foreground text-sm placeholder:text-muted-foreground px-4 py-3 focus:outline-none focus:border-[rgba(6,182,212,0.3)]"
|
||||
style={{ borderColor: 'var(--glass-border)' }}
|
||||
disabled={loading}
|
||||
/>
|
||||
<button
|
||||
onClick={handleSend}
|
||||
disabled={!input.trim() || loading}
|
||||
className="bg-gradient-brand text-[#101114] p-3 rounded-xl hover:opacity-90 active:scale-[0.97] transition-all disabled:opacity-40"
|
||||
>
|
||||
<Send size={18} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center h-full text-center">
|
||||
<div className="w-20 h-20 rounded-full bg-primary/10 flex items-center justify-center mb-4">
|
||||
<Sparkles size={32} className="text-primary" />
|
||||
</div>
|
||||
<h2 className="text-xl font-heading font-semibold text-foreground mb-2">
|
||||
AI Assistant
|
||||
</h2>
|
||||
<p className="text-sm text-muted-foreground max-w-md mb-6">
|
||||
Your Senior Systems & Network Engineer. Ask anything about IT infrastructure,
|
||||
or start a new chat to get personalized help with your team's flows.
|
||||
</p>
|
||||
<button
|
||||
onClick={handleNewChat}
|
||||
className="bg-gradient-brand text-[#101114] font-semibold text-sm rounded-[10px] px-6 py-2.5 hover:opacity-90 active:scale-[0.97] transition-all"
|
||||
>
|
||||
Start a Conversation
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user