feat: Flow Transfer, Procedural Assist & UI Design System #97
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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} />
|
||||
) : (
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
Reference in New Issue
Block a user