mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
improvement(ui/ux): chat preview
This commit is contained in:
+120
-76
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user