fix: AI Assist panel sits below topbar with slide-in animation

- Panel now uses top:56px to sit below the app shell topbar instead of
  covering it (matches the main-content grid cell area)
- Added slideInRight CSS animation for smooth drawer entrance
- Editor pages use dynamic paddingRight via PANEL_WIDTH constant
- ChatTab upgraded: markdown rendering, CopilotPanel-style message
  bubbles, auto-focus input, Shift+Enter hint
- All borders use --glass-border for consistent glassmorphism

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
chihlasm
2026-03-07 12:17:34 -05:00
parent b5fca870d1
commit 26cb64bdcc
6 changed files with 65 additions and 30 deletions

View File

@@ -1,5 +1,6 @@
import { useRef, useEffect } from 'react'
import { Send, Sparkles } from 'lucide-react'
import { Send, Sparkles, Loader2 } from 'lucide-react'
import { MarkdownContent } from '@/components/ui/MarkdownContent'
import type { EditorAIChatMessage } from '@/types'
interface ChatTabProps {
@@ -12,11 +13,17 @@ interface ChatTabProps {
export function ChatTab({ messages, input, onInputChange, onSend, isLoading }: ChatTabProps) {
const messagesEndRef = useRef<HTMLDivElement>(null)
const inputRef = useRef<HTMLTextAreaElement>(null)
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
}, [messages])
// Focus input when panel mounts
useEffect(() => {
requestAnimationFrame(() => inputRef.current?.focus())
}, [])
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault()
@@ -26,7 +33,8 @@ export function ChatTab({ messages, input, onInputChange, onSend, isLoading }: C
return (
<div className="flex flex-1 flex-col overflow-hidden">
<div className="flex-1 overflow-y-auto px-3 py-3 space-y-3">
{/* Messages */}
<div className="flex-1 overflow-y-auto px-4 py-3 space-y-3">
{messages.length === 0 && (
<div className="flex flex-col items-center justify-center h-full text-muted-foreground text-sm">
<Sparkles className="h-8 w-8 mb-2 opacity-40" />
@@ -36,44 +44,52 @@ export function ChatTab({ messages, input, onInputChange, onSend, isLoading }: C
{messages.map((msg, i) => (
<div
key={i}
className={`text-sm rounded-lg px-3 py-2 ${
msg.role === 'user'
? 'ml-6 bg-primary/15 text-foreground'
: 'mr-2 bg-[rgba(255,255,255,0.04)] border border-border text-foreground'
}`}
className={`flex ${msg.role === 'user' ? 'justify-end' : 'justify-start'}`}
>
<p className="whitespace-pre-wrap">{msg.content}</p>
<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>
))}
{isLoading && (
<div className="mr-2 bg-[rgba(255,255,255,0.04)] border border-border rounded-lg px-3 py-2">
<div className="flex gap-1">
<div className="h-1.5 w-1.5 rounded-full bg-primary/60 animate-bounce" />
<div className="h-1.5 w-1.5 rounded-full bg-primary/60 animate-bounce [animation-delay:0.15s]" />
<div className="h-1.5 w-1.5 rounded-full bg-primary/60 animate-bounce [animation-delay:0.3s]" />
<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>
)}
<div ref={messagesEndRef} />
</div>
<div className="shrink-0 border-t border-border p-3">
{/* 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) => onInputChange(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="Ask AI to help..."
rows={1}
className="flex-1 resize-none rounded-lg border border-border bg-card px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:border-[rgba(6,182,212,0.3)] focus:outline-none"
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={isLoading}
/>
<button
onClick={onSend}
disabled={!input.trim() || isLoading}
className="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-gradient-brand text-[#101114] transition-all hover:opacity-90 active:scale-[0.97] disabled:opacity-40"
className="bg-gradient-brand text-[#101114] p-2.5 rounded-xl hover:opacity-90 active:scale-[0.97] transition-all disabled:opacity-40"
>
<Send className="h-4 w-4" />
<Send size={16} />
</button>
</div>
<p className="text-[0.625rem] text-muted-foreground mt-1.5 px-1">Shift + Enter for a new line</p>
</div>
</div>
)

View File

@@ -6,6 +6,8 @@ import { ChatTab } from './ChatTab'
import { SuggestionsTab } from './SuggestionsTab'
import type { EditorAIChatMessage, AISuggestion } from '@/types'
const PANEL_WIDTH = 380
interface EditorAIPanelProps {
isOpen: boolean
onClose: () => void
@@ -23,6 +25,8 @@ interface EditorAIPanelProps {
type Tab = 'chat' | 'suggestions'
export { PANEL_WIDTH }
export function EditorAIPanel({
isOpen,
onClose,
@@ -45,28 +49,37 @@ export function EditorAIPanel({
return (
<div
className="fixed right-0 top-0 bottom-0 z-50 flex flex-col border-l"
className="fixed right-0 bottom-0 z-40 flex flex-col border-l"
style={{
width: '380px',
top: '56px',
width: `${PANEL_WIDTH}px`,
background: 'rgba(16, 17, 20, 0.95)',
backdropFilter: 'var(--glass-blur)',
WebkitBackdropFilter: 'var(--glass-blur)',
borderColor: 'var(--glass-border)',
animation: 'slideInRight 200ms ease-out',
}}
>
<div className="flex items-center justify-between border-b px-3 py-2.5 shrink-0" style={{ 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 className="h-4 w-4 text-primary" />
<span className="text-sm font-medium text-foreground">AI Assist</span>
<Sparkles size={16} className="text-primary" />
<span className="text-sm font-semibold text-foreground">AI Assist</span>
</div>
<button
onClick={onClose}
className="flex h-6 w-6 items-center justify-center rounded-md text-muted-foreground hover:bg-[rgba(255,255,255,0.06)] hover:text-foreground transition-colors"
className="p-1.5 rounded-lg hover:bg-[rgba(255,255,255,0.06)] text-muted-foreground hover:text-foreground transition-colors"
>
<X className="h-3.5 w-3.5" />
<X size={16} />
</button>
</div>
<NodeSummary node={focalNode} flowName={flowName} flowType={flowType} nodeCount={nodeCount} />
{/* Tabs */}
<div className="flex border-b shrink-0" style={{ borderColor: 'var(--glass-border)' }}>
<button
onClick={() => setActiveTab('chat')}
@@ -96,6 +109,7 @@ export function EditorAIPanel({
)}
</button>
</div>
{activeTab === 'chat' ? (
<ChatTab messages={messages} input={input} onInputChange={onInputChange} onSend={onSend} isLoading={isLoading} />
) : (

View File

@@ -22,7 +22,7 @@ const NODE_COLORS: Record<string, string> = {
export function NodeSummary({ node, flowName, flowType, nodeCount }: NodeSummaryProps) {
if (!node) {
return (
<div className="border-b border-border px-3 py-2.5">
<div className="border-b px-3 py-2.5" style={{ borderColor: 'var(--glass-border)' }}>
<div className="flex items-center gap-2">
<Layout className="h-3.5 w-3.5 text-muted-foreground" />
<span className="text-xs font-medium text-foreground truncate">
@@ -41,7 +41,7 @@ export function NodeSummary({ node, flowName, flowType, nodeCount }: NodeSummary
const colorClass = NODE_COLORS[node.type] || 'text-muted-foreground'
return (
<div className="border-b border-border px-3 py-2.5">
<div className="border-b px-3 py-2.5" style={{ borderColor: 'var(--glass-border)' }}>
<div className="flex items-center gap-2">
<Icon className={`h-3.5 w-3.5 ${colorClass}`} />
<span className="font-label text-[0.625rem] uppercase tracking-[0.1em] text-muted-foreground">

View File

@@ -162,6 +162,11 @@
to { transform: translateY(0); opacity: 1; }
}
@keyframes slideInRight {
from { transform: translateX(100%); }
to { transform: translateX(0); }
}
@keyframes fadeInRight {
from { transform: translateX(30px); opacity: 0; }
to { transform: translateX(0); opacity: 1; }

View File

@@ -10,7 +10,7 @@ import { getScheduleSummary } from '@/components/procedural-editor/scheduleUtils
import { StepList } from '@/components/procedural-editor/StepList'
import { TagInput } from '@/components/common/TagInput'
import { Spinner } from '@/components/common/Spinner'
import { EditorAIPanel } from '@/components/editor-ai/EditorAIPanel'
import { EditorAIPanel, PANEL_WIDTH } from '@/components/editor-ai/EditorAIPanel'
import { ContextMenu } from '@/components/common/ContextMenu'
import { useEditorAI } from '@/hooks/useEditorAI'
import { cn } from '@/lib/utils'
@@ -180,7 +180,7 @@ export function ProceduralEditorPage() {
}
return (
<div className={cn('flex h-full flex-col overflow-hidden transition-[padding] duration-200', editorAI.isOpen && 'pr-[380px]')}>
<div className="flex h-full flex-col overflow-hidden transition-[padding] duration-200" style={{ paddingRight: editorAI.isOpen ? `${PANEL_WIDTH}px` : 0 }}>
{/* Toolbar — sticky */}
<div className="flex shrink-0 items-center justify-between border-b border-border bg-card px-4 py-2">
<div className="flex items-center gap-3">

View File

@@ -17,7 +17,7 @@ import { cn, safeGetItem } from '@/lib/utils'
import { toast } from '@/lib/toast'
import { FlowAnalyticsPanel } from '@/components/analytics/FlowAnalyticsPanel'
import { ExportFlowModal } from '@/components/library/ExportFlowModal'
import { EditorAIPanel } from '@/components/editor-ai/EditorAIPanel'
import { EditorAIPanel, PANEL_WIDTH } from '@/components/editor-ai/EditorAIPanel'
import { ContextMenu } from '@/components/common/ContextMenu'
import { useEditorAI } from '@/hooks/useEditorAI'
import { findNodeInTree } from '@/store/treeEditorStore'
@@ -522,7 +522,7 @@ export function TreeEditorPage() {
}
return (
<div className={cn('flex h-full flex-col overflow-hidden transition-[padding] duration-200', editorAI.isOpen && 'pr-[380px]')}>
<div className="flex h-full flex-col overflow-hidden transition-[padding] duration-200" style={{ paddingRight: editorAI.isOpen ? `${PANEL_WIDTH}px` : 0 }}>
{/* Draft Restore Prompt */}
{showDraftPrompt && (