improvement(ui/ux): chat preview

This commit is contained in:
Emir Karabeg
2025-04-21 04:46:51 -07:00
parent f82d4b96f8
commit 1338da39c8
2 changed files with 160 additions and 76 deletions
@@ -1,15 +1,69 @@
'use client'
import { KeyboardEvent, useEffect, useMemo, useRef } from 'react'
import { ArrowUp, X } from 'lucide-react'
import { ArrowUp, CornerDownLeft, Loader2, X } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { cn } from '@/lib/utils'
import { useExecutionStore } from '@/stores/execution/store'
import { useChatStore } from '@/stores/panel/chat/store'
import { ChatMessage as ChatMessageType } from '@/stores/panel/chat/types'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
import { JSONView } from '@/app/w/[id]/components/panel/components/console/components/json-view/json-view'
import { useWorkflowExecution } from '@/app/w/[id]/hooks/use-workflow-execution'
interface ChatMessageProps {
message: ChatMessageType
}
// ChatGPT-style message component specifically for modal
function ModalChatMessage({ message }: ChatMessageProps) {
// Check if content is a JSON object
const isJsonObject = useMemo(() => {
return typeof message.content === 'object' && message.content !== null
}, [message.content])
// For user messages (on the right)
if (message.type === 'user') {
return (
<div className="py-5 px-4">
<div className="max-w-3xl mx-auto">
<div className="flex justify-end">
<div className="bg-[#F4F4F4] dark:bg-gray-600 rounded-3xl max-w-[80%] py-3 px-4">
<div className="whitespace-pre-wrap break-words text-base leading-relaxed text-[#0D0D0D]">
{isJsonObject ? (
<JSONView data={message.content} initiallyExpanded={false} />
) : (
<span>{message.content}</span>
)}
</div>
</div>
</div>
</div>
</div>
)
}
// For assistant messages (on the left)
return (
<div className="py-5 px-4">
<div className="max-w-3xl mx-auto">
<div className="flex">
<div className="max-w-[80%]">
<div className="whitespace-pre-wrap break-words text-base leading-relaxed">
{isJsonObject ? (
<JSONView data={message.content} initiallyExpanded={false} />
) : (
<span>{message.content}</span>
)}
</div>
</div>
</div>
</div>
</div>
)
}
interface ChatModalProps {
open: boolean
onOpenChange: (open: boolean) => void
@@ -20,6 +74,7 @@ interface ChatModalProps {
export function ChatModal({ open, onOpenChange, chatMessage, setChatMessage }: ChatModalProps) {
const messagesEndRef = useRef<HTMLDivElement>(null)
const messagesContainerRef = useRef<HTMLDivElement>(null)
const inputRef = useRef<HTMLInputElement>(null)
const { activeWorkflowId } = useWorkflowRegistry()
const { messages, addMessage } = useChatStore()
@@ -45,6 +100,13 @@ export function ChatModal({ open, onOpenChange, chatMessage, setChatMessage }: C
}
}, [workflowMessages])
// Focus input when modal opens
useEffect(() => {
if (open && inputRef.current) {
inputRef.current.focus()
}
}, [open])
// Handle send message
const handleSendMessage = async () => {
if (!chatMessage.trim() || !activeWorkflowId || isExecuting) return
@@ -78,12 +140,28 @@ export function ChatModal({ open, onOpenChange, chatMessage, setChatMessage }: C
return (
<div className="fixed inset-0 z-[100] bg-background flex flex-col">
{/* Close button (fixed position) */}
<div className="absolute top-4 left-4 z-10">
<style jsx>{`
@keyframes growShrink {
0%,
100% {
transform: scale(0.9);
}
50% {
transform: scale(1.1);
}
}
.loading-dot {
animation: growShrink 1.5s infinite ease-in-out;
}
`}</style>
{/* Header with title and close button */}
<div className="flex items-center justify-between border-b px-4 py-3">
<h2 className="text-lg font-medium">Chat</h2>
<Button
variant="ghost"
size="icon"
className="h-8 w-8 p-0 bg-background/80 hover:bg-accent/50 backdrop-blur-sm"
className="h-8 w-8 rounded-md hover:bg-accent/50"
onClick={() => onOpenChange(false)}
>
<X className="h-4 w-4" />
@@ -92,39 +170,32 @@ export function ChatModal({ open, onOpenChange, chatMessage, setChatMessage }: C
</div>
{/* Messages container */}
<div ref={messagesContainerRef} className="flex-1 overflow-y-auto pb-32">
<div>
{workflowMessages.map((message) => (
<div key={message.id} className={cn('py-6 px-4', message.type === 'user' ? '' : '')}>
<div
className={cn(
'max-w-3xl mx-auto',
message.type === 'user' ? 'flex justify-end' : ''
)}
>
<div
className={cn(
'whitespace-pre-wrap break-words leading-relaxed max-w-[80%]',
message.type === 'user'
? 'bg-[#802FFF] dark:bg-[#7028E6] text-white py-3 px-4 rounded-2xl'
: 'text-base'
)}
>
{message.content}
</div>
<div ref={messagesContainerRef} className="flex-1 overflow-y-auto">
<div className="max-w-3xl mx-auto">
{workflowMessages.length === 0 ? (
<div className="flex flex-col items-center justify-center h-full py-10 px-4">
<div className="text-center space-y-2">
<h3 className="text-lg font-medium">How can I help you today?</h3>
<p className="text-muted-foreground text-sm">
Ask me anything about your workflow.
</p>
</div>
</div>
))}
) : (
workflowMessages.map((message) => (
<ModalChatMessage key={message.id} message={message} />
))
)}
{/* Loading indicator (shows only when executing) */}
{isExecuting && (
<div className="py-6 px-4">
<div className="py-5 px-4">
<div className="max-w-3xl mx-auto">
<div className="flex items-center h-8">
<div className="typing-indicator">
<span></span>
<span></span>
<span></span>
<div className="flex">
<div className="max-w-[80%]">
<div className="flex items-center h-6">
<div className="w-3 h-3 rounded-full bg-black dark:bg-black loading-dot"></div>
</div>
</div>
</div>
</div>
@@ -136,68 +207,41 @@ export function ChatModal({ open, onOpenChange, chatMessage, setChatMessage }: C
</div>
{/* Input area (fixed at bottom) */}
<div className="fixed bottom-0 left-0 right-0 bg-gradient-to-t from-background to-background/80 pb-6">
<div className="bg-background p-4">
<div className="max-w-3xl mx-auto">
<div className="relative rounded-xl border bg-background shadow-[0_4px_12px_rgba(0,0,0,0.08)] hover:shadow-[0_4px_16px_rgba(0,0,0,0.12)] transition-shadow">
<div className="relative rounded-2xl border bg-background shadow-sm">
<Input
ref={inputRef}
value={chatMessage}
onChange={(e) => setChatMessage(e.target.value)}
onKeyDown={handleKeyPress}
placeholder="Message..."
className="flex-1 border-0 focus-visible:ring-0 focus-visible:ring-offset-0 h-14 pr-12 bg-transparent pl-4 text-base"
className="flex-1 border-0 focus-visible:ring-0 focus-visible:ring-offset-0 py-7 pr-16 bg-transparent pl-6 text-base min-h-[50px] rounded-2xl"
disabled={!activeWorkflowId || isExecuting}
/>
<Button
onClick={handleSendMessage}
size="icon"
disabled={!chatMessage.trim() || !activeWorkflowId || isExecuting}
className="absolute right-2 top-2 h-10 w-10 bg-[#802FFF] hover:bg-[#7028E6] text-white rounded-lg"
className="absolute right-3 top-1/2 -translate-y-1/2 h-10 w-10 p-0 hover:bg-gray-100 dark:hover:bg-gray-800 hover:text-gray-900 dark:hover:text-gray-100 rounded-xl"
>
<ArrowUp className="h-5 w-5" />
{isExecuting ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<ArrowUp className="h-4 w-4" />
)}
</Button>
</div>
<div className="mt-2 text-center text-xs text-muted-foreground">
<p>
{activeWorkflowId
? 'Your messages will be processed by the active workflow'
: 'Select a workflow to start chatting'}
</p>
</div>
</div>
</div>
{/* Typing indicator animation styles */}
<style jsx global>{`
.typing-indicator {
display: flex;
align-items: center;
}
.typing-indicator span {
height: 8px;
width: 8px;
margin: 0 2px;
background-color: #888;
border-radius: 50%;
display: inline-block;
opacity: 0.7;
}
.typing-indicator span:nth-child(1) {
animation: bounce 1s infinite 0.1s;
}
.typing-indicator span:nth-child(2) {
animation: bounce 1s infinite 0.3s;
}
.typing-indicator span:nth-child(3) {
animation: bounce 1s infinite 0.5s;
}
@keyframes bounce {
0%,
100% {
transform: translateY(0);
}
50% {
transform: translateY(-6px);
}
}
`}</style>
</div>
)
}
+40
View File
@@ -0,0 +1,40 @@
import OpenAI from 'openai'
/**
* Generates a short title for a chat based on the first message
* @param message First user message in the chat
* @returns A short title or null if API key is not available
*/
export async function generateChatTitle(message: string): Promise<string | null> {
const apiKey = process.env.OPENAI_API_KEY
if (!apiKey) {
return null
}
try {
const openai = new OpenAI({ apiKey })
const response = await openai.chat.completions.create({
model: 'gpt-3.5-turbo',
messages: [
{
role: 'system',
content: 'Generate a very short title (3-5 words max) for a chat that starts with this message. The title should be concise and descriptive.'
},
{
role: 'user',
content: message
}
],
max_tokens: 20,
temperature: 0.7,
})
const title = response.choices[0]?.message?.content?.trim() || null
return title
} catch (error) {
console.error('Error generating chat title:', error)
return null
}
}