Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 20 additions & 15 deletions services/platform/app/features/chat/components/chat-input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { X, Paperclip } from 'lucide-react';
import { LoaderCircleIcon } from 'lucide-react';
import { ComponentPropsWithoutRef, useRef, useMemo, useState } from 'react';

import type { Id } from '@/convex/_generated/dataModel';

import { EnterKeyIcon } from '@/app/components/icons/enter-key-icon';
import { DocumentIcon } from '@/app/components/ui/data-display/document-icon';
import { FileUpload } from '@/app/components/ui/forms/file-upload';
Expand All @@ -15,10 +17,8 @@ import {
} from '@/lib/shared/file-types';
import { cn } from '@/lib/utils/cn';

import {
useConvexFileUpload,
type FileAttachment,
} from '../hooks/use-convex-file-upload';
import type { FileAttachment } from '../hooks/use-convex-file-upload';

import { AgentSelector } from './agent-selector';
import { ImagePreviewDialog } from './message-bubble';

Expand All @@ -32,6 +32,11 @@ interface ChatInputProps extends Omit<
value?: string;
onChange?: (value: string) => void;
organizationId: string;
attachments: FileAttachment[];
uploadingFiles: string[];
uploadFiles: (files: File[]) => Promise<void>;
removeAttachment: (fileId: Id<'_storage'>) => void;
clearAttachments: () => FileAttachment[];
}

export function ChatInput({
Expand All @@ -41,6 +46,11 @@ export function ChatInput({
isLoading = false,
placeholder,
organizationId,
attachments,
uploadingFiles,
uploadFiles,
removeAttachment,
clearAttachments,
...restProps
}: ChatInputProps) {
const { t: tChat } = useT('chat');
Expand All @@ -53,14 +63,6 @@ export function ChatInput({
alt: string;
} | null>(null);

const {
attachments,
uploadingFiles,
uploadFiles,
removeAttachment,
clearAttachments,
} = useConvexFileUpload();

const defaultPlaceholder = placeholder || tChat('typeMessageHere');

const handleSendMessage = () => {
Expand Down Expand Up @@ -150,7 +152,10 @@ export function ChatInput({
{(attachments.length > 0 || uploadingFiles.length > 0) && (
<div className="mb-2 flex flex-wrap gap-1">
{imageAttachments.map((attachment) => (
<div key={attachment.fileId} className="group relative">
<div
key={attachment.fileId}
className="group relative size-11 overflow-hidden rounded-lg shadow-sm"
>
<button
type="button"
onClick={() =>
Expand All @@ -160,7 +165,7 @@ export function ChatInput({
alt: attachment.fileName,
})
}
className="bg-secondary/20 focus:ring-ring size-11 cursor-pointer overflow-hidden rounded-lg transition-opacity hover:opacity-90 focus:ring-2 focus:ring-offset-2 focus:outline-none"
className="bg-secondary/20 focus:ring-ring size-full cursor-pointer transition-opacity hover:opacity-90 focus:ring-2 focus:ring-offset-2 focus:outline-none"
>
{attachment.previewUrl ? (
<img
Expand Down Expand Up @@ -190,7 +195,7 @@ export function ChatInput({
{fileAttachments.map((attachment) => (
<div
key={attachment.fileId}
className="group bg-secondary/20 relative flex max-w-[216px] items-center gap-2 rounded-lg px-2 py-1"
className="bg-secondary/20 group relative flex max-w-[216px] items-center gap-2 rounded-lg px-2 py-1"
>
<DocumentIcon fileName={attachment.fileName} />
<div className="flex min-w-0 flex-1 flex-col">
Expand Down
44 changes: 36 additions & 8 deletions services/platform/app/features/chat/components/chat-interface.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { useRef, useEffect, useState, useCallback } from 'react';
import { FileUpload } from '@/app/components/ui/forms/file-upload';
import { Button } from '@/app/components/ui/primitives/button';
import { useAutoScroll } from '@/app/hooks/use-auto-scroll';
import { usePersistedState } from '@/app/hooks/use-persisted-state';
import { useT } from '@/lib/i18n/client';
import { cn } from '@/lib/utils/cn';

Expand All @@ -19,14 +20,20 @@ import {
useWorkflowCreationApprovals,
} from '../hooks/queries';
import { useChatPendingState } from '../hooks/use-chat-pending-state';
import { useConvexFileUpload } from '../hooks/use-convex-file-upload';
import { useMergedChatItems } from '../hooks/use-merged-chat-items';
import { useMessageProcessing } from '../hooks/use-message-processing';
import { usePendingMessages } from '../hooks/use-pending-messages';
import { usePersistedAttachments } from '../hooks/use-persisted-attachments';
import { useSendMessage } from '../hooks/use-send-message';
import { ChatInput } from './chat-input';
import { ChatMessages } from './chat-messages';
import { WelcomeView } from './welcome-view';

function chatDraftKey(threadId?: string) {
return threadId ? `chat-draft-${threadId}` : 'chat-draft-new';
}

interface ChatInterfaceProps {
organizationId: string;
threadId?: string;
Expand All @@ -46,9 +53,23 @@ export function ChatInterface({
selectedAgent,
} = useChatLayout();

const [inputValue, setInputValue] = useState('');
const [inputValue, setInputValue, clearInputValue] = usePersistedState(
chatDraftKey(threadId),
'',
);
const [showScrollButton, setShowScrollButton] = useState(false);

const {
attachments,
setAttachments,
uploadingFiles,
uploadFiles,
removeAttachment,
clearAttachments,
} = useConvexFileUpload();

usePersistedAttachments({ threadId, attachments, setAttachments });

// Message processing
const {
messages: rawMessages,
Expand Down Expand Up @@ -191,20 +212,23 @@ export function ChatInterface({

const handleSendMessage = async (
message: string,
attachments?: FileAttachment[],
sentAttachments?: FileAttachment[],
) => {
setInputValue('');
await sendMessage(message, attachments);
clearInputValue();
await sendMessage(message, sentAttachments);
};

const handleHumanInputResponseSubmitted = useCallback(() => {
setPendingWithCount(true, true);
shouldScrollToAIRef.current = true;
}, [setPendingWithCount]);

const handleSendFollowUp = useCallback((message: string) => {
setInputValue(message);
}, []);
const handleSendFollowUp = useCallback(
(message: string) => {
setInputValue(message);
},
[setInputValue],
);

// Determine what to show in content area
// Show welcome only when idle (no threadId, no messages, no pending message, not loading)
Expand Down Expand Up @@ -276,13 +300,17 @@ export function ChatInterface({
</div>
<FileUpload.Root>
<ChatInput
key={threadId || 'new-chat'}
className="mx-auto w-full max-w-(--chat-max-width)"
value={inputValue}
onChange={setInputValue}
onSendMessage={handleSendMessage}
isLoading={isLoading}
organizationId={organizationId}
attachments={attachments}
uploadingFiles={uploadingFiles}
uploadFiles={uploadFiles}
removeAttachment={removeAttachment}
clearAttachments={clearAttachments}
/>
</FileUpload.Root>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,14 +92,6 @@ function MessageBubbleComponent({
: 'text-foreground bg-background'
}`}
>
{message.attachments && message.attachments.length > 0 && (
<div className="mb-2 flex flex-wrap gap-1">
{message.attachments.map((attachment, index) => (
<FileAttachmentDisplay key={index} attachment={attachment} />
))}
</div>
)}

{message.fileParts && message.fileParts.length > 0 && (
<div className="mb-2 flex flex-wrap gap-1">
{message.fileParts.map((part, index) => (
Expand All @@ -117,6 +109,14 @@ function MessageBubbleComponent({
/>
</div>
)}

{message.attachments && message.attachments.length > 0 && (
<div className="mt-2 flex flex-wrap gap-1">
{message.attachments.map((attachment, index) => (
<FileAttachmentDisplay key={index} attachment={attachment} />
))}
</div>
)}
{!isUser && !isAssistantStreaming && (
<div className="flex items-center pt-2">
<Tooltip
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,14 +127,18 @@ export const FileAttachmentDisplay = memo(function FileAttachmentDisplay({
attachment.fileId,
!!attachment.previewUrl,
);
const displayUrl = attachment.previewUrl || serverFileUrl;
const displayUrl = attachment.previewUrl || serverFileUrl || undefined;
const isImage = attachment.fileType.startsWith('image/');

if (!displayUrl) return null;
if (isImage && !displayUrl) {
return (
<div className="bg-muted size-11 animate-pulse overflow-hidden rounded-lg" />
);
}

if (isImage) {
return (
<div className="size-11 overflow-hidden rounded-lg bg-gray-200 bg-cover bg-center bg-no-repeat">
<div className="bg-muted size-11 overflow-hidden rounded-lg bg-cover bg-center bg-no-repeat">
<img
src={displayUrl}
alt={attachment.fileName}
Expand All @@ -144,22 +148,41 @@ export const FileAttachmentDisplay = memo(function FileAttachmentDisplay({
);
}

if (!displayUrl) {
return (
<div className="bg-muted flex max-w-[216px] items-center gap-2 rounded-lg px-2 py-1.5">
<FileTypeIcon
fileType={attachment.fileType}
fileName={attachment.fileName}
/>
<div className="flex min-w-0 flex-1 flex-col">
<div className="text-foreground truncate text-sm font-medium">
{attachment.fileName}
</div>
<div className="text-muted-foreground text-xs">
{getFileTypeLabel(attachment.fileName, attachment.fileType, t)}
</div>
</div>
</div>
);
}

return (
<a
href={displayUrl}
target="_blank"
rel="noopener noreferrer"
className="flex max-w-[216px] items-center gap-2 rounded-lg bg-gray-100 px-2 py-1.5 transition-colors hover:bg-gray-200"
className="bg-muted hover:bg-muted/80 flex max-w-[216px] items-center gap-2 rounded-lg px-2 py-1.5 transition-colors"
>
<FileTypeIcon
fileType={attachment.fileType}
fileName={attachment.fileName}
/>
<div className="flex min-w-0 flex-1 flex-col">
<div className="truncate text-sm font-medium text-gray-800">
<div className="text-foreground truncate text-sm font-medium">
{attachment.fileName}
</div>
<div className="text-xs text-gray-500">
<div className="text-muted-foreground text-xs">
{getFileTypeLabel(attachment.fileName, attachment.fileType, t)}
</div>
</div>
Expand Down
Loading
Loading