8000 Feature/sync fork by Gijela · Pull Request #2 · Gijela/chat2repo · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Feature/sync fork #2

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
May 14, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

8000
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,10 @@ XAI_API_KEY=****
# Instructions to create a Vercel Blob Store here: https://vercel.com/docs/storage/vercel-blob
BLOB_READ_WRITE_TOKEN=****

# Instructions to create a database here: https://vercel.com/docs/storage/vercel-postgres/quickstart
# Instructions to create a PostgreSQL database here: https://vercel.com/docs/storage/vercel-postgres/quickstart
POSTGRES_URL=****


# Instructions to create a Redis store here:
# https://vercel.com/docs/redis
REDIS_URL=****
3 changes: 2 additions & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
"settings": {
"import/resolver": {
"typescript": {
"alwaysTryTypes": true
"alwaysTryTypes": true,
"project": "./tsconfig.json"
}
}
},
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/playwright.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ jobs:
AUTH_SECRET: ${{ secrets.AUTH_SECRET }}
POSTGRES_URL: ${{ secrets.POSTGRES_URL }}
BLOB_READ_WRITE_TOKEN: ${{ secrets.BLOB_READ_WRITE_TOKEN }}
REDIS_URL: ${{ secrets.REDIS_URL }}

steps:
- uses: actions/checkout@v4
Expand Down
132 changes: 126 additions & 6 deletions app/(chat)/api/chat/route.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
import {
appendClientMessage,
appendResponseMessages,
createDataStreamResponse,
createDataStream,
smoothStream,
streamText,
} from 'ai';
import { auth, type UserType } from '@/app/(auth)/auth';
import { systemPrompt } from '@/lib/ai/prompts';
import { type RequestHints, systemPrompt } from '@/lib/ai/prompts';
import {
createStreamId,
deleteChatById,
getChatById,
getMessageCountByUserId,
getMessagesByChatId,
getStreamIdsByChatId,
saveChat,
saveMessages,
} from '@/lib/db/queries';
Expand All @@ -25,9 +27,38 @@ import { isProductionEnvironment } from '@/lib/constants';
import { myProvider } from '@/lib/ai/providers';
import { entitlementsByUserType } from '@/lib/ai/entitlements';
import { postRequestBodySchema, type PostRequestBody } from './schema';
import { geolocation } from '@vercel/functions';
import {
createResumableStreamContext,
type ResumableStreamContext,
} from 'resumable-stream';
import { after } from 'next/server';
import type { Chat } from '@/lib/db/schema';

export const maxDuration = 60;

let globalStreamContext: ResumableStreamContext | null = null;

function getStreamContext() {
if (!globalStreamContext) {
try {
globalStreamContext = createResumableStreamContext({
waitUntil: after,
});
} catch (error: any) {
if (error.message.includes('REDIS_URL')) {
console.log(
' > Resumable streams are disabled due to missing REDIS_URL',
);
} else {
console.error(error);
}
}
}

return globalStreamContext;
}

export async function POST(request: Request) {
let requestBody: PostRequestBody;

Expand All @@ -39,7 +70,8 @@ export async function POST(request: Request) {
}

try {
const { id, message, selectedChatModel } = requestBody;
const { id, message, selectedChatModel, selectedVisibilityType } =
requestBody;

const session = await auth();

Expand Down Expand Up @@ -70,7 +102,12 @@ export async function POST(request: Request) {
message,
});

await saveChat({ id, userId: session.user.id, title });
await saveChat({
id,
userId: session.user.id,
title,
visibility: selectedVisibilityType,
});
} else {
if (chat.userId !== session.user.id) {
return new Response('Forbidden', { status: 403 });
Expand All @@ -85,6 +122,15 @@ export async function POST(request: Request) {
message,
});

const { longitude, latitude, city, country } = geolocation(request);

const requestHints: RequestHints = {
longitude,
latitude,
city,
country,
};

await saveMessages({
messages: [
{
Expand All @@ -98,11 +144,14 @@ export async function POST(request: Request) {
],
});

return createDataStreamResponse({
const streamId = generateUUID();
await createStreamId({ streamId, chatId: id });

const stream = createDataStream({
execute: (dataStream) => {
const result = streamText({
model: myProvider.languageModel(selectedChatModel),
system: systemPrompt({ selectedChatModel }),
system: systemPrompt({ selectedChatModel, requestHints }),
messages,
maxSteps: 5,
experimental_activeTools:
Expand Down Expand Up @@ -177,13 +226,83 @@ export async function POST(request: Request) {
return 'Oops, an error occurred!';
},
});

const streamContext = getStreamContext();

if (streamContext) {
return new Response(
await streamContext.resumableStream(streamId, () => stream),
);
} else {
return new Response(stream);
}
} catch (_) {
return new Response('An error occurred while processing your request!', {
status: 500,
});
}
}

export async function GET(request: Request) {
const streamContext = getStreamContext();

if (!streamContext) {
return new Response(null, { status: 204 });
}

const { searchParams } = new URL(request.url);
const chatId = searchParams.get('chatId');

if (!chatId) {
return new Response('id is required', { status: 400 });
}

const session = await auth();

if (!session?.user) {
return new Response('Unauthorized', { status: 401 });
}

let chat: Chat;

try {
chat = await getChatById({ id: chatId });
} catch {
return new Response('Not found', { status: 404 });
}

if (!chat) {
return new Response('Not found', { status: 404 });
}

if (chat.visibility === 'private' && chat.userId !== session.user.id) {
return new Response('Forbidden', { status: 403 });
}

const streamIds = await getStreamIdsByChatId({ chatId });

if (!streamIds.length) {
return new Response('No streams found', { status: 404 });
}

const recentStreamId = streamIds.at(-1);

if (!recentStreamId) {
return new Response('No recent stream found', { status: 404 });
}

const emptyDataStream = createDataStream({
execute: () => {},
});

return new Response(
await streamContext.resumableStream(recentStreamId, () => emptyDataStream),
{
status: 200,
},
);
}

export async function DELETE(request: Request) {
const { searchParams } = new URL(request.url);
const id = searchParams.get('id');
Expand All @@ -209,6 +328,7 @@ export async function DELETE(request: Request) {

return Response.json(deletedChat, { status: 200 });
} catch (error) {
console.error(error);
return new Response('An error occurred while processing your request!', {
status: 500,
});
Expand Down
1 change: 1 addition & 0 deletions app/(chat)/api/chat/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export const postRequestBodySchema = z.object({
.optional(),
}),
selectedChatModel: z.enum(['chat-model', 'chat-model-reasoning']),
selectedVisibilityType: z.enum(['public', 'private']),
});

export type PostRequestBody = z.infer<typeof postRequestBodySchema>;
10 changes: 6 additions & 4 deletions app/(chat)/chat/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,11 @@ export default async function Page(props: { params: Promise<{ id: string }> }) {
<Chat
id={chat.id}
initialMessages={convertToUIMessages(messagesFromDb)}
selectedChatModel={DEFAULT_CHAT_MODEL}
selectedVisibilityType={chat.visibility}
initialChatModel={DEFAULT_CHAT_MODEL}
initialVisibilityType={chat.visibility}
isReadonly={session?.user?.id !== chat.userId}
session={session}
autoResume={true}
/>
<DataStreamHandler id={id} />
</>
Expand All @@ -75,10 +76,11 @@ export default async function Page(props: { params: Promise<{ id: string }> }) {
<Chat
id={chat.id}
initialMessages={convertToUIMessages(messagesFromDb)}
selectedChatModel={chatModelFromCookie.value}
selectedVisibilityType={chat.visibility}
initialChatModel={chatModelFromCookie.value}
initialVisibilityType={chat.visibility}
isReadonly={session?.user?.id !== chat.userId}
session={session}
autoResume={true}
/>
<DataStreamHandler id={id} />
</>
Expand Down
10 changes: 6 additions & 4 deletions app/(chat)/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,11 @@ export default async function Page() {
key={id}
id={id}
initialMessages={[]}
selectedChatModel={DEFAULT_CHAT_MODEL}
selectedVisibilityType="private"
initialChatModel={DEFAULT_CHAT_MODEL}
initialVisibilityType="private"
isReadonly={false}
session={session}
autoResume={false}
/>
<DataStreamHandler id={id} />
</>
Expand All @@ -42,10 +43,11 @@ export default async function Page() {
key={id}
id={id}
initialMessages={[]}
selectedChatModel={modelIdFromCookie.value}
selectedVisibilityType="private"
initialChatModel={modelIdFromCookie.value}
initialVisibilityType="private"
isReadonly={false}
session={session}
autoResume={false}
/>
<DataStreamHandler id={id} />
</>
Expand Down
3 changes: 2 additions & 1 deletion biome.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@
"noUselessStringConcat": "warn", // Not in recommended ruleset, turning on manually
"noForEach": "off", // forEach is too familiar to ban
"noUselessSwitchCase": "off", // Turned off due to developer preferences
"noUselessThisAlias": "off" // Turned off due to developer preferences
"noUselessThisAlias": "off", // Turned off due to developer preferences
"noBannedTypes": "off"
},
"correctness": {
"noUnusedImports": "warn", // Not in recommended ruleset, turning on manually
Expand Down
36 changes: 27 additions & 9 deletions components/artifact-messages.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { PreviewMessage } from './message';
import { useScrollToBottom } from './use-scroll-to-bottom';
import { Vote } from '@/lib/db/schema';
import { UIMessage } from 'ai';
import { PreviewMessage, ThinkingMessage } from './message';
import type { Vote } from '@/lib/db/schema';
import type { UIMessage } from 'ai';
import { memo } from 'react';
import equal from 'fast-deep-equal';
import { UIArtifact } from './artifact';
import { UseChatHelpers } from '@ai-sdk/react';
import type { UIArtifact } from './artifact';
import type { UseChatHelpers } from '@ai-sdk/react';
import { motion } from 'framer-motion';
import { useMessages } from '@/hooks/use-messages';

interface ArtifactMessagesProps {
chatId: string;
Expand All @@ -27,8 +28,16 @@ function PureArtifactMessages({
reload,
isReadonly,
}: ArtifactMessagesProps) {
const [messagesContainerRef, messagesEndRef] =
useScrollToBottom<HTMLDivElement>();
const {
containerRef: messagesContainerRef,
endRef: messagesEndRef,
onViewportEnter,
onViewportLeave,
hasSentMessage,
} = useMessages({
chatId,
status,
});

return (
<div
Expand All @@ -49,12 +58,21 @@ function PureArtifactMessages({
setMessages={setMessages}
reload={reload}
isReadonly={isReadonly}
requiresScrollPadding={
hasSentMessage && index === messages.length - 1
}
/>
))}

<div
{status === 'submitted' &&
messages.length > 0 &&
messages[messages.length - 1].role === 'user' && <ThinkingMessage />}

<motion.div
ref={messagesEndRef}
className="shrink-0 min-w-[24px] min-h-[24px]"
>
>
/>
</div>
);
Expand Down
Loading
0