Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
12 changes: 4 additions & 8 deletions apps/server/convex/message_queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,11 +120,9 @@ export const getFirstUserMessage = query({

const message = await ctx.db
.query("messages")
.withIndex("by_chat_not_deleted", (q) =>
q.eq("chatId", args.chatId).eq("deletedAt", undefined)
.withIndex("by_chat_role_not_deleted", (q) =>
q.eq("chatId", args.chatId).eq("role", "user").eq("deletedAt", undefined)
)
.filter((q) => q.eq(q.field("role"), "user"))
.order("asc")
.first();
Comment thread
greptile-apps[bot] marked this conversation as resolved.

return message?.content ?? null;
Expand All @@ -144,11 +142,9 @@ export const getActiveStream = query({

const streamingMessage = await ctx.db
.query("messages")
.withIndex("by_chat_not_deleted", (q) =>
q.eq("chatId", args.chatId).eq("deletedAt", undefined)
.withIndex("by_chat_status", (q) =>
q.eq("chatId", args.chatId).eq("status", "streaming").eq("deletedAt", undefined)
)
.order("desc")
.filter((q) => q.eq(q.field("status"), "streaming"))
.first();
Comment thread
leoisadev1 marked this conversation as resolved.
Comment thread
leoisadev1 marked this conversation as resolved.

return streamingMessage?.streamId ?? null;
Expand Down
36 changes: 20 additions & 16 deletions apps/server/convex/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,14 +182,16 @@ export const editAndRegenerate = mutation({

const now = Date.now();

const activeStreams = await ctx.db
.query("streamJobs")
.withIndex("by_chat", (q) => q.eq("chatId", args.chatId).eq("status", "running"))
.collect();
const pendingStreams = await ctx.db
.query("streamJobs")
.withIndex("by_chat", (q) => q.eq("chatId", args.chatId).eq("status", "pending"))
.collect();
const [activeStreams, pendingStreams] = await Promise.all([
ctx.db
.query("streamJobs")
.withIndex("by_chat", (q) => q.eq("chatId", args.chatId).eq("status", "running"))
.collect(),
ctx.db
.query("streamJobs")
.withIndex("by_chat", (q) => q.eq("chatId", args.chatId).eq("status", "pending"))
.collect(),
]);

for (const stream of [...activeStreams, ...pendingStreams]) {
await ctx.db.patch(stream._id, {
Expand Down Expand Up @@ -264,14 +266,16 @@ export const retryMessage = mutation({

const now = Date.now();

const activeStreams = await ctx.db
.query("streamJobs")
.withIndex("by_chat", (q) => q.eq("chatId", args.chatId).eq("status", "running"))
.collect();
const pendingStreams = await ctx.db
.query("streamJobs")
.withIndex("by_chat", (q) => q.eq("chatId", args.chatId).eq("status", "pending"))
.collect();
const [activeStreams, pendingStreams] = await Promise.all([
ctx.db
.query("streamJobs")
.withIndex("by_chat", (q) => q.eq("chatId", args.chatId).eq("status", "running"))
.collect(),
ctx.db
.query("streamJobs")
.withIndex("by_chat", (q) => q.eq("chatId", args.chatId).eq("status", "pending"))
.collect(),
]);
for (const stream of [...activeStreams, ...pendingStreams]) {
await ctx.db.patch(stream._id, {
status: "completed",
Expand Down
2 changes: 2 additions & 0 deletions apps/server/convex/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ export default defineSchema({
.index("by_user", ["userId"])
.index("by_user_status", ["userId", "status", "createdAt"])
.index("by_chat_not_deleted", ["chatId", "deletedAt", "createdAt"])
.index("by_chat_role_not_deleted", ["chatId", "role", "deletedAt", "createdAt"])
.index("by_user_created", ["userId", "createdAt"])
.index("by_stream_id", ["streamId"])
.index("by_chat_status", ["chatId", "status", "deletedAt"]),
Expand Down Expand Up @@ -300,6 +301,7 @@ export default defineSchema({
})
.index("by_chat", ["chatId", "status"])
.index("by_user", ["userId", "status"])
.index("by_user_status_created", ["userId", "status", "createdAt"])
.index("by_status", ["status", "createdAt"]),
benchmarks: defineTable({
openRouterModelId: v.string(),
Expand Down
40 changes: 21 additions & 19 deletions apps/server/convex/streamJobs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -288,29 +288,31 @@ export const cleanupStaleJobs = mutation({
},
handler: async (ctx, args) => {
const userId = await requireAuthUserId(ctx, args.userId);
const staleJobs = await ctx.db
.query("streamJobs")
.withIndex("by_user", (q) => q.eq("userId", userId))
.filter((q) =>
q.or(
q.eq(q.field("status"), "running"),
q.eq(q.field("status"), "pending")
)
)
.collect();

const fiveMinutesAgo = Date.now() - 5 * 60 * 1000;
const [runningJobs, pendingJobs] = await Promise.all([
ctx.db
.query("streamJobs")
.withIndex("by_user_status_created", (q) =>
q.eq("userId", userId).eq("status", "running").lt("createdAt", fiveMinutesAgo)
)
Comment thread
leoisadev1 marked this conversation as resolved.
Outdated
.collect(),
ctx.db
.query("streamJobs")
.withIndex("by_user_status_created", (q) =>
q.eq("userId", userId).eq("status", "pending").lt("createdAt", fiveMinutesAgo)
)
.collect(),
]);
const staleJobs = [...runningJobs, ...pendingJobs];
let cleaned = 0;

for (const job of staleJobs) {
if (job.createdAt < fiveMinutesAgo) {
await ctx.db.patch(job._id, {
status: "error",
error: "Cleaned up stale job",
completedAt: Date.now(),
});
cleaned++;
}
await ctx.db.patch(job._id, {
status: "error",
error: "Cleaned up stale job",
completedAt: Date.now(),
});
cleaned++;
}

return { cleaned, total: staleJobs.length };
Expand Down
2 changes: 1 addition & 1 deletion apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"node": ">=22"
},
"scripts": {
"dev": "BROWSERSLIST_IGNORE_OLD_DATA=true BASELINE_BROWSER_MAPPING_IGNORE_OLD_DATA=true sh -c 'portless \"${PORTLESS_NAME:-openchat}\" vite dev'",
"dev": "BROWSERSLIST_IGNORE_OLD_DATA=true BASELINE_BROWSER_MAPPING_IGNORE_OLD_DATA=true sh -c 'portless openchat vite dev'",
Comment thread
leoisadev1 marked this conversation as resolved.
Outdated
"build": "vite build",
"build:vercel": "bun ./scripts/vercel-build.ts",
"start": "node .output/server/index.mjs",
Expand Down
91 changes: 64 additions & 27 deletions apps/web/src/components/__tests__/app-sidebar.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,16 @@ vi.mock('@/lib/auth-client', () => ({
useAuth: vi.fn(() => ({ user: null })),
}))

let mockConvexUserState = {
convexUser: null as { _id: string } | null,
convexUserId: undefined as string | undefined,
isLoading: false,
}

vi.mock('@/lib/convex-user', () => ({
useConvexUser: vi.fn(() => mockConvexUserState),
}))

vi.mock('@/stores/provider', () => ({
useProviderStore: vi.fn((selector: (s: any) => any) =>
selector({ activeProvider: 'osschat' }),
Expand Down Expand Up @@ -192,6 +202,11 @@ describe('AppSidebar', () => {
beforeEach(() => {
vi.mocked(useAuth).mockReturnValue({ user: null } as any)
vi.mocked(useQuery).mockReturnValue(undefined as any)
mockConvexUserState = {
convexUser: null,
convexUserId: undefined,
isLoading: false,
}
vi.mocked(useNavigate).mockReturnValue(vi.fn())
vi.mocked(useParams).mockReturnValue({} as any)
vi.mocked(useSidebar).mockReturnValue({
Expand Down Expand Up @@ -238,37 +253,49 @@ describe('AppSidebar', () => {

it('shows "No chats yet" empty state when authenticated but chat list is empty', () => {
vi.mocked(useAuth).mockReturnValue({ user: mockUser } as any)
vi.mocked(useQuery)
.mockReturnValueOnce(mockConvexUser as any)
.mockReturnValueOnce({ chats: [] } as any)
mockConvexUserState = {
convexUser: mockConvexUser,
convexUserId: mockConvexUser._id,
isLoading: false,
}
vi.mocked(useQuery).mockReturnValue({ chats: [] } as any)
render(<AppSidebar />)
expect(screen.getByText('No chats yet')).toBeTruthy()
})

it('renders chat titles when chats are available', () => {
vi.mocked(useAuth).mockReturnValue({ user: mockUser } as any)
vi.mocked(useQuery)
.mockReturnValueOnce(mockConvexUser as any)
.mockReturnValueOnce({ chats: [todayChat, anotherTodayChat] } as any)
mockConvexUserState = {
convexUser: mockConvexUser,
convexUserId: mockConvexUser._id,
isLoading: false,
}
vi.mocked(useQuery).mockReturnValue({ chats: [todayChat, anotherTodayChat] } as any)
render(<AppSidebar />)
expect(screen.getByText('Chat about AI')).toBeTruthy()
expect(screen.getByText('React hooks discussion')).toBeTruthy()
})

it('shows "Today" group label for chats updated today', () => {
vi.mocked(useAuth).mockReturnValue({ user: mockUser } as any)
vi.mocked(useQuery)
.mockReturnValueOnce(mockConvexUser as any)
.mockReturnValueOnce({ chats: [todayChat] } as any)
mockConvexUserState = {
convexUser: mockConvexUser,
convexUserId: mockConvexUser._id,
isLoading: false,
}
vi.mocked(useQuery).mockReturnValue({ chats: [todayChat] } as any)
render(<AppSidebar />)
expect(screen.getByText('Today')).toBeTruthy()
})

it('shows "Last 7 days" group label for chats from the past week', () => {
vi.mocked(useAuth).mockReturnValue({ user: mockUser } as any)
vi.mocked(useQuery)
.mockReturnValueOnce(mockConvexUser as any)
.mockReturnValueOnce({ chats: [lastWeekChat] } as any)
mockConvexUserState = {
convexUser: mockConvexUser,
convexUserId: mockConvexUser._id,
isLoading: false,
}
vi.mocked(useQuery).mockReturnValue({ chats: [lastWeekChat] } as any)
render(<AppSidebar />)
expect(screen.getByText('Last 7 days')).toBeTruthy()
})
Expand All @@ -292,9 +319,7 @@ describe('AppSidebar', () => {

it('shows user name and Settings link when user is authenticated', () => {
vi.mocked(useAuth).mockReturnValue({ user: mockUser } as any)
vi.mocked(useQuery)
.mockReturnValueOnce(null as any)
.mockReturnValueOnce(undefined as any)
vi.mocked(useQuery).mockReturnValue(undefined as any)
render(<AppSidebar />)
expect(screen.getByText('Test User')).toBeTruthy()
expect(screen.getByText('Settings')).toBeTruthy()
Expand All @@ -314,9 +339,12 @@ describe('AppSidebar', () => {

it('renders a delete button for each chat item', () => {
vi.mocked(useAuth).mockReturnValue({ user: mockUser } as any)
vi.mocked(useQuery)
.mockReturnValueOnce(mockConvexUser as any)
.mockReturnValueOnce({ chats: [todayChat, anotherTodayChat] } as any)
mockConvexUserState = {
convexUser: mockConvexUser,
convexUserId: mockConvexUser._id,
isLoading: false,
}
vi.mocked(useQuery).mockReturnValue({ chats: [todayChat, anotherTodayChat] } as any)
render(<AppSidebar />)
const deleteButtons = screen.getAllByRole('button', { name: /delete chat/i })
expect(deleteButtons.length).toBe(2)
Expand All @@ -338,9 +366,12 @@ describe('AppSidebar', () => {

it('clicking a chat item navigates to /c/$chatId', () => {
vi.mocked(useAuth).mockReturnValue({ user: mockUser } as any)
vi.mocked(useQuery)
.mockReturnValueOnce(mockConvexUser as any)
.mockReturnValueOnce({ chats: [todayChat] } as any)
mockConvexUserState = {
convexUser: mockConvexUser,
convexUserId: mockConvexUser._id,
isLoading: false,
}
vi.mocked(useQuery).mockReturnValue({ chats: [todayChat] } as any)
const mockNavigate = vi.fn()
vi.mocked(useNavigate).mockReturnValue(mockNavigate)
render(<AppSidebar />)
Expand All @@ -363,9 +394,12 @@ describe('AppSidebar', () => {
} as any)
vi.mocked(useNavigate).mockReturnValue(mockNavigate)
vi.mocked(useAuth).mockReturnValue({ user: mockUser } as any)
vi.mocked(useQuery)
.mockReturnValueOnce(mockConvexUser as any)
.mockReturnValueOnce({ chats: [todayChat] } as any)
mockConvexUserState = {
convexUser: mockConvexUser,
convexUserId: mockConvexUser._id,
isLoading: false,
}
vi.mocked(useQuery).mockReturnValue({ chats: [todayChat] } as any)
render(<AppSidebar />)
const chatBtns = screen.getAllByTestId('sidebar-menu-button')
fireEvent.click(chatBtns[0])
Expand Down Expand Up @@ -457,9 +491,12 @@ describe('AppSidebar', () => {

it('dismissing the delete chat dialog clears the delete state', () => {
vi.mocked(useAuth).mockReturnValue({ user: mockUser } as any)
vi.mocked(useQuery)
.mockReturnValueOnce(mockConvexUser as any)
.mockReturnValueOnce({ chats: [todayChat] } as any)
mockConvexUserState = {
convexUser: mockConvexUser,
convexUserId: mockConvexUser._id,
isLoading: false,
}
vi.mocked(useQuery).mockReturnValue({ chats: [todayChat] } as any)
render(<AppSidebar />)

const deleteBtn = screen.getAllByRole('button', { name: /delete chat/i })[0]
Expand Down
11 changes: 4 additions & 7 deletions apps/web/src/components/app-sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
useSidebar,
} from "./ui/sidebar";
import { useAuth } from "@/lib/auth-client";
import { useConvexUser } from "@/lib/convex-user";
import { convexClient } from "@/lib/convex";
import { useProviderStore } from "@/stores/provider";
import { useChatTitleStore } from "@/stores/chat-title";
Expand All @@ -34,6 +35,7 @@ export function AppSidebar({
...props
}: React.ComponentProps<typeof Sidebar>) {
const { user } = useAuth();
const { convexUser, convexUserId, isLoading: isConvexUserLoading } = useConvexUser();
const { open, isMobile, setOpen, setOpenMobile } = useSidebar();
const navigate = useNavigate();
const activeProvider = useProviderStore((s) => s.activeProvider);
Expand All @@ -50,14 +52,9 @@ export function AppSidebar({
// Not on a chat page
}

const convexUser = useQuery(
api.users.getByExternalId,
convexClient && user?.id ? { externalId: user.id } : "skip",
);

const chatsResult = useQuery(
api.chats.list,
convexClient && convexUser?._id ? { userId: convexUser._id } : "skip",
convexClient && convexUserId ? { userId: convexUserId } : "skip",
);

const cachedChatsRef = useRef<Array<ChatItem> | null>(null);
Expand Down Expand Up @@ -94,7 +91,7 @@ export function AppSidebar({
const hasCachedChats = chats.length > 0;
const isLoadingChats =
user?.id && !hasCachedChats
? convexUser === undefined || chatsResult === undefined
? isConvexUserLoading || chatsResult === undefined
: false;

const dayKey = new Date().toDateString();
Expand Down
Loading