* 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>
181 lines
6.5 KiB
TypeScript
181 lines
6.5 KiB
TypeScript
import { useState, useRef, useEffect, useCallback } from 'react'
|
|
import { X, Send, Sparkles, Loader2 } from 'lucide-react'
|
|
import { MarkdownContent } from '@/components/ui/MarkdownContent'
|
|
import { copilotApi } from '@/api/copilot'
|
|
import { SuggestedFlowCard } from '@/components/assistant/SuggestedFlowCard'
|
|
import type { CopilotMessage, SuggestedFlow } from '@/types/copilot'
|
|
|
|
interface CopilotPanelProps {
|
|
isOpen: boolean
|
|
onClose: () => void
|
|
treeId: string
|
|
sessionId?: string
|
|
currentNodeId?: string
|
|
}
|
|
|
|
export function CopilotPanel({ isOpen, onClose, treeId, sessionId, currentNodeId }: CopilotPanelProps) {
|
|
const [conversationId, setConversationId] = useState<string | null>(null)
|
|
const [messages, setMessages] = useState<CopilotMessage[]>([])
|
|
const [suggestedFlows, setSuggestedFlows] = useState<SuggestedFlow[]>([])
|
|
const [input, setInput] = useState('')
|
|
const [loading, setLoading] = useState(false)
|
|
const [initializing, setInitializing] = useState(false)
|
|
const messagesEndRef = useRef<HTMLDivElement>(null)
|
|
const inputRef = useRef<HTMLTextAreaElement>(null)
|
|
|
|
const startConversation = useCallback(async () => {
|
|
setInitializing(true)
|
|
try {
|
|
const response = await copilotApi.startConversation({
|
|
tree_id: treeId,
|
|
session_id: sessionId,
|
|
current_node_id: currentNodeId,
|
|
})
|
|
setConversationId(response.conversation_id)
|
|
setMessages([{ role: 'assistant', content: response.greeting }])
|
|
} catch {
|
|
setMessages([{ role: 'assistant', content: 'Failed to start copilot. Please try again.' }])
|
|
} finally {
|
|
setInitializing(false)
|
|
}
|
|
}, [treeId, sessionId, currentNodeId])
|
|
|
|
// Start conversation when panel opens or treeId changes
|
|
useEffect(() => {
|
|
if (isOpen && !conversationId && !initializing) {
|
|
startConversation()
|
|
}
|
|
}, [isOpen, treeId, startConversation, conversationId, initializing])
|
|
|
|
// Auto-scroll to bottom
|
|
useEffect(() => {
|
|
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
|
|
}, [messages])
|
|
|
|
const handleSend = async () => {
|
|
if (!input.trim() || !conversationId || loading) return
|
|
|
|
const userMessage = input.trim()
|
|
setInput('')
|
|
setMessages(prev => [...prev, { role: 'user', content: userMessage }])
|
|
setLoading(true)
|
|
|
|
try {
|
|
const response = await copilotApi.sendMessage(conversationId, {
|
|
message: userMessage,
|
|
current_node_id: currentNodeId,
|
|
})
|
|
setMessages(prev => [...prev, { role: 'assistant', content: response.content }])
|
|
if (response.suggested_flows.length > 0) {
|
|
setSuggestedFlows(response.suggested_flows)
|
|
}
|
|
} 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()
|
|
}
|
|
}
|
|
|
|
if (!isOpen) return null
|
|
|
|
return (
|
|
<div
|
|
className="fixed right-0 top-0 bottom-0 z-50 flex flex-col border-l"
|
|
style={{
|
|
width: '400px',
|
|
background: 'rgba(16, 17, 20, 0.95)',
|
|
backdropFilter: 'var(--glass-blur)',
|
|
WebkitBackdropFilter: 'var(--glass-blur)',
|
|
borderColor: 'var(--glass-border)',
|
|
}}
|
|
>
|
|
{/* Header */}
|
|
<div
|
|
className="flex items-center justify-between px-4 py-3 border-b shrink-0"
|
|
style={{ borderColor: 'var(--glass-border)' }}
|
|
>
|
|
<div className="flex items-center gap-2">
|
|
<Sparkles size={16} className="text-primary" />
|
|
<span className="text-sm font-semibold text-foreground">AI Copilot</span>
|
|
</div>
|
|
<button
|
|
onClick={onClose}
|
|
className="p-1.5 rounded-lg hover:bg-[rgba(255,255,255,0.06)] text-muted-foreground hover:text-foreground transition-colors"
|
|
>
|
|
<X size={16} />
|
|
</button>
|
|
</div>
|
|
|
|
{/* Messages */}
|
|
<div className="flex-1 overflow-y-auto px-4 py-3 space-y-3">
|
|
{messages.map((msg, i) => (
|
|
<div key={i} className={`flex ${msg.role === 'user' ? 'justify-end' : 'justify-start'}`}>
|
|
<div
|
|
className={`max-w-[85%] rounded-xl px-3.5 py-2.5 text-[0.8125rem] leading-relaxed ${
|
|
msg.role === 'user'
|
|
? 'bg-primary/15 text-foreground'
|
|
: 'bg-[rgba(255,255,255,0.04)] text-foreground border border-[rgba(255,255,255,0.06)]'
|
|
}`}
|
|
>
|
|
<MarkdownContent content={msg.content} className="text-[0.8125rem] leading-relaxed" />
|
|
</div>
|
|
</div>
|
|
))}
|
|
{loading && (
|
|
<div className="flex justify-start">
|
|
<div className="bg-[rgba(255,255,255,0.04)] border border-[rgba(255,255,255,0.06)] rounded-xl px-3.5 py-2.5">
|
|
<Loader2 size={16} className="animate-spin text-primary" />
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Suggested flows */}
|
|
{suggestedFlows.length > 0 && (
|
|
<div className="space-y-2 pt-2">
|
|
<span className="font-label text-[0.625rem] uppercase tracking-[0.1em] text-muted-foreground">
|
|
Related Flows
|
|
</span>
|
|
{suggestedFlows.map(flow => (
|
|
<SuggestedFlowCard key={flow.tree_id} flow={flow} />
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
<div ref={messagesEndRef} />
|
|
</div>
|
|
|
|
{/* Input */}
|
|
<div className="px-4 py-3 border-t shrink-0" style={{ borderColor: 'var(--glass-border)' }}>
|
|
<div className="flex items-end gap-2">
|
|
<textarea
|
|
ref={inputRef}
|
|
value={input}
|
|
onChange={e => setInput(e.target.value)}
|
|
onKeyDown={handleKeyDown}
|
|
placeholder="Ask about this step..."
|
|
rows={1}
|
|
className="flex-1 resize-none rounded-xl border bg-card text-foreground text-[0.8125rem] placeholder:text-muted-foreground px-3.5 py-2.5 focus:outline-none focus:border-[rgba(6,182,212,0.3)]"
|
|
style={{ borderColor: 'var(--glass-border)' }}
|
|
disabled={loading || initializing}
|
|
/>
|
|
<button
|
|
onClick={handleSend}
|
|
disabled={!input.trim() || loading || initializing}
|
|
className="bg-gradient-brand text-[#101114] p-2.5 rounded-xl hover:opacity-90 active:scale-[0.97] transition-all disabled:opacity-40"
|
|
>
|
|
<Send size={16} />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|