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
41 changes: 41 additions & 0 deletions app/(chat)/adapter.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Ok, Err, type Result } from 'ts-results-es';

import type { ApiGetConversationMessagesResponse } from '@/app/(chat)/types';
import { extractErrorMessageOrDefault } from '@/lib/utils';

import type {
Expand Down Expand Up @@ -55,6 +56,44 @@ export const getConversation = async (
}
};

/**
* Get messages of a conversation
* @param accessToken
* @param projectId
* @param conversationId
* @returns result containing the conversation messages
*/
export const getConversationMessages = async (
accessToken: string,
projectId: string,
conversationId: string,
): Promise<Result<ApiGetConversationMessagesResponse, string>> => {
try {
const conversationResponse = await fetch(
`${patternCoreEndpoint}/playground/conversation/${projectId}/${conversationId}`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
},
);

if (conversationResponse.ok) {
const conversationMessages: ApiGetConversationMessagesResponse = (
await conversationResponse.json()
).metadata.history;

return Ok(conversationMessages);
}
return Err(
`Fetching conversation messages failed with error code ${conversationResponse.status}`,
);
} catch (error) {
return Err(extractErrorMessageOrDefault(error));
}
};

/**
* Create a conversation
* @param accessToken
Expand All @@ -65,6 +104,7 @@ export const getConversation = async (
export const createConversation = async (
accessToken: string,
projectId: string,
conversationId: string,
conversationName: string,
): Promise<Result<ApiCreateConversationResponse, string>> => {
try {
Expand All @@ -79,6 +119,7 @@ export const createConversation = async (
body: JSON.stringify({
name: conversationName,
project_id: projectId,
conversation_id: conversationId,
}),
},
);
Expand Down
72 changes: 34 additions & 38 deletions app/(chat)/chat/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,64 +1,60 @@
import { cookies } from 'next/headers';
import { notFound } from 'next/navigation';

import { auth } from '@/app/(auth)/auth';
import { Chat } from '@/components/chat';
import { getChatById, getMessagesByChatId } from '@/lib/db/queries';
import { convertToUIMessages } from '@/lib/utils';
import { DataStreamHandler } from '@/components/data-stream-handler';
import { DEFAULT_CHAT_MODEL } from '@/lib/ai/models';
import { convertToUIMessages } from '@/lib/utils';

import { getConversation, getConversationMessages } from '../../service';

export default async function Page(props: { params: Promise<{ id: string }> }) {
const params = await props.params;
const { id } = params;
const chat = await getChatById({ id });
const session = await auth();

if (!chat) {
notFound();
if (
!session ||
!session.chainId ||
!session.address ||
!session.accessToken
) {
return notFound();
}

const session = await auth();
const { id } = params;
const chatResult = await getConversation(
session.accessToken,
session.projectId,
id,
);

if (chat.visibility === 'private') {
if (!session || !session.user) {
return notFound();
}
if (chatResult.isErr()) {
return chatResult.unwrap();
}

if (session.user.id !== chat.userId) {
return notFound();
}
const chat = chatResult.value;
if (!chat) {
return notFound();
}

const messagesFromDb = await getMessagesByChatId({
const messagesResult = await getConversationMessages(
session.accessToken,
session.projectId,
id,
});

const cookieStore = await cookies();
const chatModelFromCookie = cookieStore.get('chat-model');
);

if (!chatModelFromCookie) {
return (
<>
<Chat
id={chat.id}
initialMessages={convertToUIMessages(messagesFromDb)}
selectedChatModel={DEFAULT_CHAT_MODEL}
selectedVisibilityType={chat.visibility}
isReadonly={session?.user?.id !== chat.userId}
/>
<DataStreamHandler id={id} />
</>
);
if (messagesResult.isErr()) {
return messagesResult.unwrap();
}

const messages = messagesResult.value;

return (
<>
<Chat
id={chat.id}
initialMessages={convertToUIMessages(messagesFromDb)}
selectedChatModel={chatModelFromCookie.value}
selectedVisibilityType={chat.visibility}
isReadonly={session?.user?.id !== chat.userId}
initialMessages={convertToUIMessages(messages)}
selectedVisibilityType="private"
isReadonly={false}
/>
<DataStreamHandler id={id} />
</>
Expand Down
25 changes: 1 addition & 24 deletions app/(chat)/page.tsx
Original file line number Diff line number Diff line change
@@ -1,39 +1,16 @@
import { cookies } from 'next/headers';

import { Chat } from '@/components/chat';
import { DEFAULT_CHAT_MODEL } from '@/lib/ai/models';
import { generateUUID } from '@/lib/utils';
import { DataStreamHandler } from '@/components/data-stream-handler';
import { generateUUID } from '@/lib/utils';

export default async function Page() {
const id = generateUUID();

const cookieStore = await cookies();
const modelIdFromCookie = cookieStore.get('chat-model');

if (!modelIdFromCookie) {
return (
<>
<Chat
key={id}
id={id}
initialMessages={[]}
selectedChatModel={DEFAULT_CHAT_MODEL}
selectedVisibilityType="private"
isReadonly={false}
/>
<DataStreamHandler id={id} />
</>
);
}

return (
<>
<Chat
key={id}
id={id}
initialMessages={[]}
selectedChatModel={modelIdFromCookie.value}
selectedVisibilityType="private"
isReadonly={false}
/>
Expand Down
8 changes: 7 additions & 1 deletion app/(chat)/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export const getOrCreateConversation = async (
const createConversationResult = await createConversation(
accessToken,
projectId,
conversationId,
'Default Title',
);
if (createConversationResult.isErr()) {
Expand All @@ -40,4 +41,9 @@ export const getOrCreateConversation = async (
return Ok(conversation);
};

export { sendMessage, sendMessageStreamed } from './adapter';
export {
sendMessage,
sendMessageStreamed,
getConversation,
getConversationMessages,
} from './adapter';
6 changes: 6 additions & 0 deletions app/(chat)/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,13 @@ export interface Conversation {
project_id: string;
}

export interface Message {
role: 'human' | 'ai';
content: string;
}

export type ApiGetConversationResponse = Conversation | null;
export type ApiGetConversationMessagesResponse = Message[];
export type ApiCreateConversationResponse = Conversation;
export type ApiSendMessageResponse = string;
export type ApiSendMessageStreamedResponse = ReadableStream;
38 changes: 18 additions & 20 deletions components/chat.tsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,28 @@
"use client";
'use client';

import type { Attachment, Message } from "ai";
import { useChat } from "ai/react";
import { useState } from "react";
import useSWR, { useSWRConfig } from "swr";
import type { Attachment, Message } from 'ai';
import { useChat } from 'ai/react';
import { useState } from 'react';
import { toast } from 'sonner';
import useSWR, { useSWRConfig } from 'swr';

import { ChatHeader } from "@/components/chat-header";
import type { Vote } from "@/lib/db/schema";
import { fetcher, generateUUID } from "@/lib/utils";
import { ChatHeader } from '@/components/chat-header';
import { useArtifactSelector } from '@/hooks/use-artifact';
import type { Vote } from '@/lib/db/schema';
import { fetcher, generateUUID } from '@/lib/utils';

import { Artifact } from "./artifact";
import { MultimodalInput } from "./multimodal-input";
import { Messages } from "./messages";
import { VisibilityType } from "./visibility-selector";
import { useArtifactSelector } from "@/hooks/use-artifact";
import { toast } from "sonner";
import { Artifact } from './artifact';
import { Messages } from './messages';
import { MultimodalInput } from './multimodal-input';
import type { VisibilityType } from './visibility-selector';

export function Chat({
id,
initialMessages,
selectedChatModel,
isReadonly,
}: {
id: string;
initialMessages: Array<Message>;
selectedChatModel: string;
selectedVisibilityType: VisibilityType;
isReadonly: boolean;
}) {
Expand All @@ -42,22 +40,22 @@ export function Chat({
reload,
} = useChat({
id,
body: { id, selectedChatModel: selectedChatModel },
body: { id },
initialMessages,
experimental_throttle: 100,
sendExtraMessageFields: true,
generateId: generateUUID,
onFinish: () => {
mutate("/api/history");
mutate('/api/history');
},
onError: (error) => {
toast.error("An error occured, please try again!");
toast.error('An error occured, please try again!');
},
});

const { data: votes } = useSWR<Array<Vote>>(
`/api/vote?chatId=${id}`,
fetcher
fetcher,
);

const [attachments, setAttachments] = useState<Array<Attachment>>([]);
Expand Down
63 changes: 17 additions & 46 deletions lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ import type {
CoreAssistantMessage,
CoreToolMessage,
Message,
ToolInvocation,
} from 'ai';
import { type ClassValue, clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';

import type { Message as DBMessage, Document } from '@/lib/db/schema';
import type { Message as CoreMessage } from '@/app/(chat)/types';
import type { Document } from '@/lib/db/schema';

export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
Expand Down Expand Up @@ -83,50 +83,21 @@ function addToolMessageToChat({
});
}

export function convertToUIMessages(
messages: Array<DBMessage>,
): Array<Message> {
return messages.reduce((chatMessages: Array<Message>, message) => {
if (message.role === 'tool') {
return addToolMessageToChat({
toolMessage: message as CoreToolMessage,
messages: chatMessages,
});
}

let textContent = '';
let reasoning: string | undefined = undefined;
const toolInvocations: Array<ToolInvocation> = [];

if (typeof message.content === 'string') {
textContent = message.content;
} else if (Array.isArray(message.content)) {
for (const content of message.content) {
if (content.type === 'text') {
textContent += content.text;
} else if (content.type === 'tool-call') {
toolInvocations.push({
state: 'call',
toolCallId: content.toolCallId,
toolName: content.toolName,
args: content.args,
});
} else if (content.type === 'reasoning') {
reasoning = content.reasoning;
}
}
}

chatMessages.push({
id: message.id,
role: message.role as Message['role'],
content: textContent,
reasoning,
toolInvocations,
});

return chatMessages;
}, []);
export function convertToUIMessages(messages: CoreMessage[]): Array<Message> {
return messages.reduce(
(chatMessages: Array<Message>, message, index) => [
// biome-ignore lint:‌ premature optimization
...chatMessages,
{
id: `${index}`,
role: message.role === 'ai' ? 'assistant' : 'user',
content: message.content,
reasoning: '',
toolInvocations: [],
},
],
[],
);
}

type ResponseMessageWithoutId = CoreToolMessage | CoreAssistantMessage;
Expand Down
Loading