Skip to content

Commit c045c5a

Browse files
Merge pull request #26 from NillionNetwork/feat/image-input-indicator
Attachment input indicator
2 parents 5649cce + d15d663 commit c045c5a

File tree

7 files changed

+138
-70
lines changed

7 files changed

+138
-70
lines changed

src/app/api/createMessage/route.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,15 @@ export async function POST(request: NextRequest) {
1313
const auth = await requireAuth(request);
1414

1515
const body = await request.json();
16-
const { chat_id, role, order, timestamp, model, blindfoldContent } = body;
16+
const {
17+
chat_id,
18+
role,
19+
order,
20+
timestamp,
21+
model,
22+
blindfoldContent,
23+
attachments,
24+
} = body;
1725

1826
if (!chat_id || !role || !blindfoldContent || order === undefined) {
1927
return NextResponse.json(
@@ -42,6 +50,7 @@ export async function POST(request: NextRequest) {
4250
timestamp: timestamp,
4351
model: model,
4452
signature: "",
53+
attachments,
4554
};
4655

4756
const builder = await setupClient();

src/app/app/chat/[id]/page.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ export default function ChatPage({ params }: ChatPageProps) {
5252
return {
5353
...message,
5454
content: result.content,
55+
attachments: message?.attachments,
5556
};
5657
}),
5758
);

src/components/chat/ChatMessage.tsx

Lines changed: 44 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { useState } from "react";
66
import ReactMarkdown from "react-markdown";
77
import remarkGfm from "remark-gfm";
88
import type { ChatMessage as MessageType } from "../../types/chat";
9+
import getMessageAttachmentIcon from "../../utils/getMessageAttachmentIcon";
910

1011
interface ChatMessageProps {
1112
message: MessageType;
@@ -49,35 +50,52 @@ const ChatMessage: React.FC<ChatMessageProps> = ({
4950
`;
5051

5152
return (
52-
<div className={`flex ${isUser ? "justify-end" : "justify-start"} mb-2`}>
53-
<div className={bubbleClasses}>
54-
<div className={markdownProseClasses}>
55-
<ReactMarkdown remarkPlugins={[remarkGfm]}>
56-
{message.content as string}
57-
</ReactMarkdown>
58-
{isStreaming && !isUser && (
59-
<span className="inline-block w-2 h-4 bg-gray-400 animate-pulse ml-1 align-text-bottom" />
60-
)}
53+
<>
54+
{message.attachments && message.attachments.length > 0 && (
55+
<div className="flex gap-1 items-center !-mb-2 ml-auto w-fit">
56+
<span className="text-neutral-500 text-xs">Attached</span>
57+
<div className="flex gap-1 items-center">
58+
{message.attachments.map((attachment) => {
59+
const AttachmentIcon = getMessageAttachmentIcon(attachment);
60+
return (
61+
<div key={attachment}>
62+
<AttachmentIcon size={12} className="text-neutral-500" />
63+
</div>
64+
);
65+
})}
66+
</div>
6167
</div>
62-
63-
{!isUser && (
64-
<div className="flex mt-2 space-x-2">
65-
<button
66-
onClick={() => copyToClipboard(message.content as string)}
67-
className="p-1 text-neutral-500 hover:text-neutral-700"
68-
title={isCopied ? "Copied!" : "Copy to clipboard"}
69-
>
70-
<Image
71-
src={isCopied ? "/img/tick_icon.svg" : "/img/copy-icon.png"}
72-
width={12}
73-
height={12}
74-
alt={isCopied ? "copied" : "copy-icon"}
75-
/>
76-
</button>
68+
)}
69+
<div className={`flex ${isUser ? "justify-end" : "justify-start"} mb-2`}>
70+
<div className={bubbleClasses}>
71+
<div className={markdownProseClasses}>
72+
<ReactMarkdown remarkPlugins={[remarkGfm]}>
73+
{message.content as string}
74+
</ReactMarkdown>
75+
{isStreaming && !isUser && (
76+
<span className="inline-block w-2 h-4 bg-gray-400 animate-pulse ml-1 align-text-bottom" />
77+
)}
7778
</div>
78-
)}
79+
80+
{!isUser && (
81+
<div className="flex mt-2 space-x-2">
82+
<button
83+
onClick={() => copyToClipboard(message.content as string)}
84+
className="p-1 text-neutral-500 hover:text-neutral-700"
85+
title={isCopied ? "Copied!" : "Copy to clipboard"}
86+
>
87+
<Image
88+
src={isCopied ? "/img/tick_icon.svg" : "/img/copy-icon.png"}
89+
width={12}
90+
height={12}
91+
alt={isCopied ? "copied" : "copy-icon"}
92+
/>
93+
</button>
94+
</div>
95+
)}
96+
</div>
7997
</div>
80-
</div>
98+
</>
8199
);
82100
};
83101

src/components/chat/StreamingChatArea.tsx

Lines changed: 58 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { useEncryption } from "@/hooks/useEncryption";
99
import { LocalStorageService } from "@/services/LocalStorage";
1010
import { useStreamingChat } from "../../hooks/useStreamingChat";
1111
import type { ChatMessage as MessageType } from "../../types/chat";
12+
import type { TMessageAttachment } from "../../types/schemas";
1213
import ChatInput from "./ChatInput";
1314
import ChatMessage from "./ChatMessage";
1415

@@ -209,12 +210,15 @@ const StreamingChatArea: React.FC<StreamingChatAreaProps> = ({
209210
}
210211
};
211212

212-
const createMessage = async (
213-
chatId: string,
214-
role: "user" | "assistant",
215-
content: string,
216-
order: number,
217-
) => {
213+
const createMessage = async (message: {
214+
chatId: string;
215+
role: "user" | "assistant";
216+
content: string;
217+
order: number;
218+
attachments?: TMessageAttachment[];
219+
}) => {
220+
const { chatId, role, content, order, attachments } = message;
221+
218222
if (!user) {
219223
console.error("No authenticated user found");
220224
return;
@@ -246,6 +250,7 @@ const StreamingChatArea: React.FC<StreamingChatAreaProps> = ({
246250
model: DEFAULT_MODEL,
247251
creator: user?.id,
248252
blindfoldContent: blindfoldContent,
253+
attachments,
249254
}),
250255
});
251256

@@ -317,10 +322,17 @@ const StreamingChatArea: React.FC<StreamingChatAreaProps> = ({
317322
? userMessage.content
318323
: userMessage.content.find((content) => content.type === "text")
319324
?.text || "";
325+
const userMessageAttachments: TMessageAttachment[] = imageDataUrl
326+
? ["image"]
327+
: [];
320328

321329
setMessages((prev) => [
322330
...prev,
323-
{ ...userMessage, content: userMessageContentText },
331+
{
332+
...userMessage,
333+
content: userMessageContentText,
334+
attachments: userMessageAttachments,
335+
},
324336
]);
325337

326338
setTimeout(() => scrollToBottom(true), 100);
@@ -366,37 +378,39 @@ const StreamingChatArea: React.FC<StreamingChatAreaProps> = ({
366378
chatIdRef.current = newChatId.message;
367379

368380
// Add the two messages to the chat
369-
await createMessage(
370-
newChatId.message,
371-
"user",
372-
userMessageContentText,
373-
1,
374-
);
375-
await createMessage(
376-
newChatId.message,
377-
"assistant",
378-
finalContent,
379-
2,
380-
);
381+
await createMessage({
382+
chatId: newChatId.message,
383+
role: "user",
384+
content: userMessageContentText,
385+
order: 1,
386+
attachments: userMessageAttachments,
387+
});
388+
await createMessage({
389+
chatId: newChatId.message,
390+
role: "assistant",
391+
content: finalContent,
392+
order: 2,
393+
});
381394
}
382395

383396
if (totalMessages === 4) {
384397
const currentChatId = chatIdRef.current || chatId;
385398

386399
if (currentChatId) {
387400
await Promise.all([
388-
createMessage(
389-
currentChatId,
390-
"user",
391-
userMessageContentText,
392-
totalMessages - 1,
393-
),
394-
createMessage(
395-
currentChatId,
396-
"assistant",
397-
finalContent,
398-
totalMessages,
399-
),
401+
createMessage({
402+
chatId: currentChatId,
403+
role: "user",
404+
content: userMessageContentText,
405+
order: totalMessages - 1,
406+
attachments: userMessageAttachments,
407+
}),
408+
createMessage({
409+
chatId: currentChatId,
410+
role: "assistant",
411+
content: finalContent,
412+
order: totalMessages,
413+
}),
400414
]);
401415

402416
setIsUpdatingChat(true);
@@ -426,22 +440,23 @@ const StreamingChatArea: React.FC<StreamingChatAreaProps> = ({
426440
await Promise.all([
427441
// ODD (User)
428442
(totalMessages - 1) % 2 === 1
429-
? createMessage(
430-
currentChatId,
431-
"user",
432-
userMessageContentText,
433-
totalMessages - 1,
434-
)
443+
? createMessage({
444+
chatId: currentChatId,
445+
role: "user",
446+
content: userMessageContentText,
447+
order: totalMessages - 1,
448+
attachments: userMessageAttachments,
449+
})
435450
: Promise.resolve(),
436451

437452
// EVEN (Assistant)
438453
totalMessages % 2 === 0
439-
? createMessage(
440-
currentChatId,
441-
"assistant",
442-
finalContent,
443-
totalMessages,
444-
)
454+
? createMessage({
455+
chatId: currentChatId,
456+
role: "assistant",
457+
content: finalContent,
458+
order: totalMessages,
459+
})
445460
: Promise.resolve(),
446461
]);
447462

src/types/chat.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import type { TMessageAttachment } from "./schemas";
2+
13
export interface ITextContent {
24
type: "text";
35
text: string;
@@ -15,4 +17,5 @@ export type IChatMessageContent = ITextContent | IImageContent;
1517
export interface ChatMessage {
1618
role: "user" | "assistant";
1719
content: string | IChatMessageContent[];
20+
attachments?: TMessageAttachment[];
1821
}

src/types/schemas.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ export interface CHAT_SCHEMA {
1616
persona?: string;
1717
}
1818

19+
export type TMessageAttachment = "image" | "pdf" | "csv" | "audio";
20+
1921
export interface MESSAGES_SCHEMA {
2022
_id: string;
2123
chat_id: string;
@@ -29,4 +31,5 @@ export interface MESSAGES_SCHEMA {
2931
model: string;
3032
signature: string;
3133
token_count?: string;
34+
attachments?: TMessageAttachment[];
3235
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { FileIcon, ImageIcon, Mic, Paperclip } from "lucide-react";
2+
import type { TMessageAttachment } from "../types/schemas";
3+
4+
const AttachmentIcon: Record<TMessageAttachment, React.ElementType> = {
5+
image: ImageIcon,
6+
pdf: FileIcon,
7+
csv: FileIcon,
8+
audio: Mic,
9+
};
10+
11+
const getMessageAttachmentIcon = (attachment: TMessageAttachment) => {
12+
if (!AttachmentIcon[attachment]) {
13+
return Paperclip;
14+
}
15+
16+
return AttachmentIcon[attachment];
17+
};
18+
19+
export default getMessageAttachmentIcon;

0 commit comments

Comments
 (0)