Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
e7b134e
Add workspace functionality and refactor chat structure
BaoNguyen09 Dec 19, 2025
ef7b5fc
feat: Redirect to login page instead of auto-creating guest users
BaoNguyen09 Dec 20, 2025
a5c4f7d
feat: Enhance chat API with workspace membership verification
BaoNguyen09 Dec 20, 2025
7b2aa95
feat: Add workspaceId to post request body schema for chat API
BaoNguyen09 Dec 20, 2025
a4f689e
feat: Implement chatId handling and workspace membership verification…
BaoNguyen09 Dec 20, 2025
81adeca
feat: Add workspaceId validation and enhance chat retrieval with work…
BaoNguyen09 Dec 20, 2025
2d413c3
feat: Implement GET endpoint for retrieving messages by chatId with a…
BaoNguyen09 Dec 20, 2025
b67dc07
feat: Update profile API to handle POST requests with chatId and work…
BaoNguyen09 Dec 20, 2025
ff2897d
feat: Add API endpoints for managing workspaces with authorization ch…
BaoNguyen09 Dec 20, 2025
6224269
feat: Integrate workspace functionality into chat and memory graph co…
BaoNguyen09 Dec 20, 2025
dcdc387
refactor: Simplify initial conversation structure by removing superme…
BaoNguyen09 Dec 20, 2025
0ed1236
feat: Enhance error handling and message queue with workspace integra…
BaoNguyen09 Dec 20, 2025
da3bc6f
feat: Add onboarding message and setup channel creation during worksp…
BaoNguyen09 Dec 20, 2025
63cd325
feat: Adjust conversation sorting to pin Profile at the top and maint…
BaoNguyen09 Dec 20, 2025
81886c3
feat: Enhance chat loading by fetching last messages and improving co…
BaoNguyen09 Dec 20, 2025
3f10029
feat: Implement invitation acceptance and retrieval API endpoints
BaoNguyen09 Dec 21, 2025
f158e5c
feat: Add GET and PATCH endpoints for chat retrieval and title update
BaoNguyen09 Dec 22, 2025
809b5da
feat: Refactor chat deletion and initial conversation creation for wo…
BaoNguyen09 Dec 22, 2025
70deb84
feat: Add functions to get and delete invitations by workspace ID, an…
BaoNguyen09 Dec 22, 2025
a1ab832
feat: Add InviteButton component and integrate it into the navigation…
BaoNguyen09 Dec 22, 2025
83af94f
feat: Add POST endpoint for creating chats with validation and member…
BaoNguyen09 Dec 22, 2025
49507a0
feat: Implement WorkspaceSwitcher component with create and rename fu…
BaoNguyen09 Dec 22, 2025
aebea7d
feat: Enhance chat management with create, rename, and workspace-spec…
BaoNguyen09 Dec 22, 2025
c7a193c
feat: Enhance login and registration flows with redirect handling aft…
BaoNguyen09 Dec 22, 2025
207db1e
feat: Add chat connection management with create, retrieve, and delet…
BaoNguyen09 Dec 23, 2025
087cf68
feat: Implement Supermemory client for chat connection management
BaoNguyen09 Dec 23, 2025
fbf77f5
feat: Add ChatConnection table and foreign key constraints
BaoNguyen09 Dec 23, 2025
e6588b2
refactor: Update deleteChatConnection function to use connectionId in…
BaoNguyen09 Dec 23, 2025
d768da2
feat: Extend error handling for chat connections
BaoNguyen09 Dec 23, 2025
e91c520
feat: Implement chat connection management endpoints
BaoNguyen09 Dec 23, 2025
d9b57df
feat: Update supermemory language model to 'claude-haiku-4-5-20251001'
BaoNguyen09 Dec 23, 2025
0c3519b
feat: Enhance supermemory connection management and add sync function…
BaoNguyen09 Dec 23, 2025
3b84bc9
feat: Add data source connection options to chat and conversation hea…
BaoNguyen09 Dec 23, 2025
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
17 changes: 13 additions & 4 deletions app/(auth)/login/page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use client';

import Link from 'next/link';
import { useRouter } from 'next/navigation';
import { useRouter, useSearchParams } from 'next/navigation';
import { useActionState, useEffect, useState } from 'react';
import { toast } from '@/components/toast';

Expand All @@ -13,6 +13,8 @@ import { useSession } from 'next-auth/react';

export default function Page() {
const router = useRouter();
const searchParams = useSearchParams();
const redirectUrl = searchParams.get('redirect');

const [email, setEmail] = useState('');
const [isSuccessful, setIsSuccessful] = useState(false);
Expand All @@ -39,10 +41,17 @@ export default function Page() {
});
} else if (state.status === 'success') {
setIsSuccessful(true);
updateSession();
router.refresh();

// Update session and navigate
updateSession().then(() => {
if (redirectUrl) {
router.push(redirectUrl);
} else {
router.push('/');
}
});
}
}, [state.status]);
}, [state.status, router, redirectUrl]);

const handleSubmit = (formData: FormData) => {
setEmail(formData.get('email') as string);
Expand Down
16 changes: 12 additions & 4 deletions app/(auth)/register/page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use client';

import Link from 'next/link';
import { useRouter } from 'next/navigation';
import { useRouter, useSearchParams } from 'next/navigation';
import { useActionState, useEffect, useState } from 'react';

import { AuthForm } from '@/components/auth-form';
Expand All @@ -13,6 +13,8 @@ import { useSession } from 'next-auth/react';

export default function Page() {
const router = useRouter();
const searchParams = useSearchParams();
const redirectUrl = searchParams.get('redirect');

const [email, setEmail] = useState('');
const [isSuccessful, setIsSuccessful] = useState(false);
Expand Down Expand Up @@ -40,10 +42,16 @@ export default function Page() {
toast({ type: 'success', description: 'Account created successfully!' });

setIsSuccessful(true);
updateSession();
router.refresh();
// Update session and navigate
updateSession().then(() => {
if (redirectUrl) {
router.push(redirectUrl);
} else {
router.push('/');
}
});
}
}, [state]);
}, [state.status, router, redirectUrl]);

const handleSubmit = (formData: FormData) => {
setEmail(formData.get('email') as string);
Expand Down
104 changes: 104 additions & 0 deletions app/(chat)/api/chat/[id]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { auth } from '@/app/(auth)/auth';
import { NextResponse } from 'next/server';
import { getChatById, updateChatTitleById, getWorkspaceMember } from '@/lib/db/queries';
import { ChatSDKError } from '@/lib/errors';
import { z } from 'zod';

const updateChatSchema = z.object({
title: z.string().min(1).max(200),
});

export async function GET(
request: Request,
{ params }: { params: Promise<{ id: string }> },
) {
const session = await auth();
if (!session?.user) {
return new ChatSDKError('unauthorized:chat').toResponse();
}

const { id: chatId } = await params;

// Get the chat
const chatData = await getChatById({ id: chatId });
if (!chatData) {
return new ChatSDKError('not_found:chat', 'Chat not found').toResponse();
}

// Check if user is a member of the workspace
const member = await getWorkspaceMember({
workspaceId: chatData.workspaceId,
userId: session.user.id,
});

if (!member) {
return new ChatSDKError(
'forbidden:chat',
'Not a member of this workspace',
).toResponse();
}

return NextResponse.json({
chat: chatData,
});
}

export async function PATCH(
request: Request,
{ params }: { params: Promise<{ id: string }> },
) {
const session = await auth();
if (!session?.user) {
return new ChatSDKError('unauthorized:chat').toResponse();
}

const { id: chatId } = await params;

// Get the chat to verify workspace membership
const chatData = await getChatById({ id: chatId });
if (!chatData) {
return new ChatSDKError('not_found:chat', 'Chat not found').toResponse();
}

// Check if user is a member of the workspace
const member = await getWorkspaceMember({
workspaceId: chatData.workspaceId,
userId: session.user.id,
});

if (!member) {
return new ChatSDKError(
'forbidden:chat',
'Not a member of this workspace',
).toResponse();
}

let body: z.infer<typeof updateChatSchema>;
try {
const json = await request.json();
body = updateChatSchema.parse(json);
} catch (e) {
return new ChatSDKError('bad_request:api', String(e)).toResponse();
}

try {
const updatedChat = await updateChatTitleById({
chatId,
title: body.title,
});

if (!updatedChat) {
return new ChatSDKError('not_found:chat', 'Chat not found').toResponse();
}

return NextResponse.json({
chat: updatedChat,
});
} catch (error) {
console.error('[Chat API] Error updating chat:', error);
return new ChatSDKError(
'bad_request:database',
'Failed to update chat',
).toResponse();
}
}
19 changes: 5 additions & 14 deletions app/(chat)/api/chat/delete-all/route.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { NextResponse } from 'next/server';
import { auth } from '@/app/(auth)/auth';
import { deleteMessagesByChatId } from '@/lib/db/queries';
import { getUserSpecificProfileId } from '@/data/initial-conversations';

export async function DELETE(request: Request) {
try {
Expand All @@ -17,10 +16,12 @@ export async function DELETE(request: Request) {
return new NextResponse('Chat ID is required', { status: 400 });
}

// Get the container tag to delete from Supermemory
const containerTag = session.user.id;
// Get the container tag to delete from Supermemory (now using chatId)
const containerTag = chatId;
console.log(
'[Delete All] Starting deletion for containerTag:',
'[Delete All] Starting deletion for chat:',
chatId,
'with containerTag:',
containerTag,
);

Expand Down Expand Up @@ -87,16 +88,6 @@ export async function DELETE(request: Request) {
// Delete conversation history from the current chat
await deleteMessagesByChatId({ id: chatId });

// Also delete the profile chat messages since the profile data is now gone
const profileChatId = getUserSpecificProfileId(session.user.id);
try {
await deleteMessagesByChatId({ id: profileChatId });
console.log('[Delete All] Cleared profile chat messages');
} catch (error) {
console.error('[Delete All] Error clearing profile chat:', error);
// Don't fail the whole operation if profile chat deletion fails
}

return NextResponse.json(
{
success: true,
Expand Down
95 changes: 35 additions & 60 deletions app/(chat)/api/chat/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
getChatById,
getMessageCountByUserId,
getMessagesByChatId,
getWorkspaceMember,
saveChat,
saveMessages,
} from '@/lib/db/queries';
Expand All @@ -41,14 +42,24 @@ export async function POST(request: Request) {
}

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

const session = await auth();
if (!session?.user) {
return new ChatSDKError('unauthorized:chat').toResponse();
}

// Verify workspace membership
const member = await getWorkspaceMember({
workspaceId,
userId: session.user.id,
});

if (!member) {
return new ChatSDKError('forbidden:chat', 'Not a member of this workspace').toResponse();
}

const userType: UserType = session.user.type;
const messageCount = await getMessageCountByUserId({
id: session.user.id,
Expand All @@ -63,13 +74,14 @@ export async function POST(request: Request) {
if (!chat) {
await saveChat({
id,
userId: session.user.id,
workspaceId,
createdBy: session.user.id,
title: '',
visibility: selectedVisibilityType,
});
} else {
if (chat.userId !== session.user.id) {
return new ChatSDKError('forbidden:chat').toResponse();
if (chat.workspaceId !== workspaceId) {
return new ChatSDKError('forbidden:chat', 'Chat does not belong to this workspace').toResponse();
}
}

Expand Down Expand Up @@ -115,6 +127,7 @@ export async function POST(request: Request) {
{
chatId: id,
id: message.id,
userId: session.user.id,
role: 'user',
parts: message.parts,
attachments:
Expand Down Expand Up @@ -145,59 +158,9 @@ export async function POST(request: Request) {
).toResponse();
}

// Always use user ID as container tag
const containerTag = session.user.id;
console.log('[Chat API] Using container tag:', containerTag);

// Check if user has existing memories to determine if they're new
let isNewUser = true;

// Only check for existing memories if this is the first message in a new conversation
if (previousMessages.length === 0) {
try {
console.log(
'[Chat API] Checking for existing memories for user:',
containerTag,
);
const baseUrl =
process.env.SUPERMEMORY_BASE_URL || 'https://api.supermemory.ai';
const profileResponse = await fetch(`${baseUrl}/v4/profile`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${supermemoryApiKey}`,
},
body: JSON.stringify({
containerTag: containerTag,
}),
});

if (profileResponse.ok) {
const profileData = await profileResponse.json();
// If profile exists and has content, user is not new
if (profileData?.profile.length > 0) {
isNewUser = false;
console.log(
'[Chat API] User has existing memories, not a new user',
);
} else {
console.log(
'[Chat API] No existing memories found, treating as new user',
);
}
}
} catch (error) {
console.error(
'[Chat API] Error checking for existing memories:',
error,
);
// Default to treating as existing user if check fails to avoid unnecessary onboarding
isNewUser = false;
}
} else {
// If there are previous messages in this conversation, definitely not a new user
isNewUser = false;
}
// Channel-scoped memory: containerTag = channel ID for isolation
const containerTag = id;
console.log('[Chat API] Using channel-scoped container:', containerTag);

// Create tools
const memoryTools = createMemoryTools(supermemoryApiKey, containerTag);
Expand Down Expand Up @@ -234,7 +197,7 @@ export async function POST(request: Request) {

const result = streamText({
model: modelWithMemory,
system: systemPrompt({ selectedChatModel, requestHints, isNewUser }),
system: systemPrompt({ selectedChatModel, requestHints }),
messages: convertedMessages,
tools: toolsConfig,
stopWhen: stepCountIs(3), // Allows up to 3 steps for tool calls and responses
Expand All @@ -259,6 +222,7 @@ export async function POST(request: Request) {
(messageText, index) => ({
id: generateUUID(),
chatId: id,
userId: null,
role: 'assistant' as const,
parts: [{ type: 'text' as const, text: messageText }],
attachments: [],
Expand All @@ -275,6 +239,7 @@ export async function POST(request: Request) {
{
id: generateUUID(),
chatId: id,
userId: null,
role: 'assistant',
parts: [{ type: 'text', text: cleanedText }],
attachments: [],
Expand Down Expand Up @@ -323,8 +288,18 @@ export async function DELETE(request: Request) {
}

const chat = await getChatById({ id });
if (chat.userId !== session.user.id) {
return new ChatSDKError('forbidden:chat').toResponse();
if (!chat) {
return new ChatSDKError('bad_request:api', 'Chat not found').toResponse();
}

// Verify workspace membership
const member = await getWorkspaceMember({
workspaceId: chat.workspaceId,
userId: session.user.id,
});

if (!member) {
return new ChatSDKError('forbidden:chat', 'Not a member of this workspace').toResponse();
}

const deletedChat = await deleteChatById({ id });
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 @@ -14,6 +14,7 @@ const filePartSchema = z.object({

export const postRequestBodySchema = z.object({
id: z.string().uuid(),
workspaceId: z.string().uuid(),
message: z.object({
id: z.string().uuid(),
createdAt: z.coerce.date(),
Expand Down
Loading