Skip to content
Open
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
70 changes: 56 additions & 14 deletions Limits.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,68 @@
// PROJECT FREE
// ============================================================
// Project Limits - Free Tier
// ============================================================

/** Maximum number of projects a free-tier user can create */
export const maxNumberOfProjectsFree = 1;
export const maxProjectSizeFree = 25000;
export const maxNumberOfProjectChatsFree = 3;

// PROJECT PRO
/** Maximum project size in characters for free-tier users */
export const maxProjectSizeFree = 25_000;

/** Maximum number of chats per project for free-tier users */
export const maxNumberOfProjectChatsFree = 3;

// ============================================================
// Project Limits - Pro Tier
// ============================================================

/** Maximum number of projects a pro-tier user can create */
export const maxNumberOfProjectsPro = 15;
export const maxProjectSizePro = 100000;

/** Maximum project size in characters for pro-tier users */
export const maxProjectSizePro = 100_000;

/** Maximum number of chats per project for pro-tier users */
export const maxNumberOfProjectChatsPro = 100;

// REGENERATE ARCHITECTURE FREE
// ============================================================
// Architecture Regeneration Limits - Free Tier
// ============================================================

/** Maximum number of files changed allowed per regeneration for free-tier users */
export const maxFilesChangedFree = 50;
export const maxLinesChangedFree = 5000;

/** Maximum number of lines changed allowed per regeneration for free-tier users */
export const maxLinesChangedFree = 5_000;

/** Maximum number of architecture regenerations for free-tier users */
export const maxFreeArchitectureRegenerations = 5;

// REGENERATE ARCHITECTURE PRO
// ============================================================
// Architecture Regeneration Limits - Pro Tier
// ============================================================

/** Maximum number of files changed allowed per regeneration for pro-tier users */
export const maxFilesChangedPro = 300;
export const maxLinesChangedPro = 50000;

// CHAT FREE
/** Maximum number of lines changed allowed per regeneration for pro-tier users */
export const maxLinesChangedPro = 50_000;

// ============================================================
// Chat Limits - Free Tier
// ============================================================

/** Maximum number of chats for free-tier users */
export const maxFreeChats = 3;
export const maxChatCharactersLimitFree = 20000;

// CHAT PRO
export const maxProChats = 100;
export const maxChatCharactersLimitPro = 100000;
/** Maximum total characters across all messages in a chat for free-tier users */
export const maxChatCharactersLimitFree = 20_000;

// ============================================================
// Chat Limits - Pro Tier
// ============================================================

/** Maximum number of chats for pro-tier users */
export const maxProChats = 100;

/** Maximum total characters across all messages in a chat for pro-tier users */
export const maxChatCharactersLimitPro = 100_000;
5 changes: 3 additions & 2 deletions actions/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import { db } from "@/lib/db";
import { auth } from "@clerk/nextjs/server";
import { revalidatePath } from "next/cache";
import { CHAT_TITLE_FROM_MESSAGE_LENGTH } from "@/constants";

export interface ChatMessage {
id: string;
Expand Down Expand Up @@ -50,8 +51,8 @@ export async function createChatWithId(chatId: string, initialMessage: string) {
id: chatId,
userId: userId,
messages: [initialMessageObj] as any,
title: initialMessage.length > 50
? initialMessage.substring(0, 50) + "..."
title: initialMessage.length > CHAT_TITLE_FROM_MESSAGE_LENGTH
? initialMessage.substring(0, CHAT_TITLE_FROM_MESSAGE_LENGTH) + "..."
: initialMessage,
},
});
Expand Down
11 changes: 6 additions & 5 deletions actions/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { ChatOpenAI } from "@langchain/openai";
import { PromptTemplate } from "@langchain/core/prompts";
import { StringOutputParser } from "@langchain/core/output_parsers";
import { generateEasyMediumPrompt, generateNthProjectPhase, generateProjectPlanDocs, initialDocsGenerationPrompt, ultraProjectChatBotPrompt } from "../prompts/ReverseArchitecture";
import { LATEST_ARCHITECTURES_FETCH_LIMIT, CHAT_TITLE_PREVIEW_LENGTH } from "@/constants";
const openaiKey = process.env.OPENAI_API_KEY;
const llm = new ChatOpenAI({
openAIApiKey: openaiKey,
Expand Down Expand Up @@ -68,7 +69,7 @@ export async function getProject(projectId: string) {
orderBy: {
createdAt: 'desc', // Latest first
},
take: 5, // Only fetch latest 5
take: LATEST_ARCHITECTURES_FETCH_LIMIT,
},
detailedAnalysis: true,
ProjectChat: {
Expand Down Expand Up @@ -333,8 +334,8 @@ export async function addMessageToProjectChat(projectId: string, chatId: string,

// If this is a user message and it's the first message in the chat, update the title
if (message.type === 'user' && currentMessages.length === 0) {
const title = message.content.length > 20
? message.content.substring(0, 20) + '...'
const title = message.content.length > CHAT_TITLE_PREVIEW_LENGTH
? message.content.substring(0, CHAT_TITLE_PREVIEW_LENGTH) + '...'
: message.content;
updateData.title = title;
}
Expand Down Expand Up @@ -663,8 +664,8 @@ export async function saveInitialMessageForInngestRevArchitecture(
};

// Title from initial message
const title = initialMessage.length > 20
? initialMessage.substring(0, 20) + '...'
const title = initialMessage.length > CHAT_TITLE_PREVIEW_LENGTH
? initialMessage.substring(0, CHAT_TITLE_PREVIEW_LENGTH) + '...'
: initialMessage;

const updatedProjectChat = await db.projectChat.update({
Expand Down
19 changes: 14 additions & 5 deletions actions/projectDocs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,22 @@ import { webResearchAgentPrompt, summarizeProjectDocsContextPrompt } from "../pr
import openai from "openai";

import { RunnableLambda } from "@langchain/core/runnables";
import {
DEFAULT_MAX_INPUT_TOKENS,
DEFAULT_MAX_OUTPUT_TOKENS,
WEB_SEARCH_MAX_INPUT_TOKENS,
WEB_SEARCH_MAX_OUTPUT_TOKENS,
MIN_REMAINING_TOKENS_THRESHOLD,
FALLBACK_TRUNCATION_LENGTH,
TOKEN_TRUNCATION_SAFETY_MARGIN,
} from "@/constants";

// Custom context window manager class
class ContextWindowManager {
private maxInputTokens: number;
private maxOutputTokens: number;

constructor(maxInputTokens: number = 8000, maxOutputTokens: number = 2000) {
constructor(maxInputTokens: number = DEFAULT_MAX_INPUT_TOKENS, maxOutputTokens: number = DEFAULT_MAX_OUTPUT_TOKENS) {
this.maxInputTokens = maxInputTokens;
this.maxOutputTokens = maxOutputTokens;
}
Expand All @@ -37,7 +46,7 @@ class ContextWindowManager {
if (estimatedTokens <= maxTokens) return text;

const ratio = maxTokens / estimatedTokens;
const truncatedLength = Math.floor(text.length * ratio * 0.9); // 10% safety margin
const truncatedLength = Math.floor(text.length * ratio * TOKEN_TRUNCATION_SAFETY_MARGIN);
return text.substring(0, truncatedLength) + "...[truncated]";
}

Expand All @@ -56,7 +65,7 @@ class ContextWindowManager {
if (totalTokens + messageTokens > this.maxInputTokens) {
// Truncate this message to fit remaining space
const remainingTokens = this.maxInputTokens - totalTokens;
if (remainingTokens > 100) { // Only include if we have reasonable space
if (remainingTokens > MIN_REMAINING_TOKENS_THRESHOLD) {
content = this.truncateToTokens(content, remainingTokens);
processedMessages.unshift({ ...message, content });
}
Expand Down Expand Up @@ -132,7 +141,7 @@ export async function summarizeProjectDocsContext(userQuery: string, projectFram

export async function generateWebSearchDocs(summarizedContext: string, framework: string) {

const contextManager = new ContextWindowManager(12000, 5000);
const contextManager = new ContextWindowManager(WEB_SEARCH_MAX_INPUT_TOKENS, WEB_SEARCH_MAX_OUTPUT_TOKENS);



Expand Down Expand Up @@ -169,7 +178,7 @@ export async function generateWebSearchDocs(summarizedContext: string, framework
} catch (error) {
console.error("Context window exceeded:", error);
// Fallback with even more aggressive truncation
const veryShortContext = truncatedContext.substring(0, 1000);
const veryShortContext = truncatedContext.substring(0, FALLBACK_TRUNCATION_LENGTH);
const response = await chain.invoke({
summarizedContext: veryShortContext,
framework: framework
Expand Down
60 changes: 38 additions & 22 deletions src/app/project/[projectId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,24 @@ import { submitFeedback } from '../../../../actions/feedback';
import { maxChatCharactersLimitFree, maxChatCharactersLimitPro, maxFreeChats, maxNumberOfProjectChatsFree, maxNumberOfProjectChatsPro } from '../../../../Limits';
import useUserSubscription from '@/hooks/useSubscription';
import PricingDialog from '@/components/PricingDialog';
import {
POLLING_MAX_ATTEMPTS,
POLLING_INITIAL_PHASE_DURATION_MS,
POLLING_INITIAL_INTERVAL_MS,
POLLING_FINAL_INTERVAL_MS,
POSITION_SAVE_DEBOUNCE_MS,
FEEDBACK_SUCCESS_CLOSE_DELAY_MS,
COPY_FEEDBACK_RESET_DELAY_MS,
TEXTAREA_MAX_HEIGHT_PX,
PANEL_MIN_WIDTH_PERCENT,
PANEL_MAX_WIDTH_PERCENT,
MOBILE_BREAKPOINT_PX,
TREE_INDENT_PER_DEPTH_PX,
TREE_DIRECTORY_PADDING_PX,
TREE_FILE_PADDING_PX,
CHAT_TITLE_PREVIEW_LENGTH,
MAX_PROMPTS_PER_CHAT,
} from '@/constants';

interface ProjectChat {
id: bigint;
Expand Down Expand Up @@ -174,7 +192,7 @@ const ProjectPage = () => {
setTimeout(() => {
setIsFeedbackOpen(false);
setFeedbackMessage(null);
}, 2000);
}, FEEDBACK_SUCCESS_CLOSE_DELAY_MS);
} else {
setFeedbackMessage({
type: 'error',
Expand Down Expand Up @@ -304,7 +322,7 @@ const ProjectPage = () => {
className={`flex items-center w-full py-1 px-2 rounded text-left group transition-colors ${
isHighlighted ? highlightStyles.bg : 'hover:bg-gray-800/50'
}`}
style={{ paddingLeft: `${depth * 16 + 8}px` }}
style={{ paddingLeft: `${depth * TREE_INDENT_PER_DEPTH_PX + TREE_DIRECTORY_PADDING_PX}px` }}
>
<ChevronRight
className={`h-3 w-3 mr-1 transition-transform duration-200 ${isExpanded ? 'rotate-90' : ''} ${
Expand Down Expand Up @@ -340,7 +358,7 @@ const ProjectPage = () => {
className={`flex items-center py-1 px-2 rounded cursor-default group transition-colors ${
isHighlighted ? highlightStyles.bg : 'hover:bg-gray-800/50'
}`}
style={{ paddingLeft: `${depth * 16 + 24}px` }}
style={{ paddingLeft: `${depth * TREE_INDENT_PER_DEPTH_PX + TREE_FILE_PADDING_PX}px` }}
title={filePath}
>
{getFileIcon(fileName)}
Expand Down Expand Up @@ -447,7 +465,7 @@ const ProjectPage = () => {
clearTimeout(debounceTimerRef.current);
}

// Debounced save to database (only save after user stops dragging for 500ms)
// Debounced save to database (only save after user stops dragging)
debounceTimerRef.current = setTimeout(async () => {
if (projectId) {
try {
Expand All @@ -456,16 +474,16 @@ const ProjectPage = () => {
console.error('Failed to save positions:', error);
}
}
}, 500);
}, POSITION_SAVE_DEBOUNCE_MS);
};

// Function to poll for project architecture completion
const pollForProjectArchitecture = async () => {
const maxAttempts = 120; // Poll for up to 10 minutes total
const maxAttempts = POLLING_MAX_ATTEMPTS;
let attempts = 0;
const initialPhaseDuration = 4 * 60 * 1000; // 4 minutes in milliseconds
const initialPollInterval = 15 * 1000; // 15 seconds for first 4 minutes
const finalPollInterval = 5 * 1000; // 5 seconds after 4 minutes
const initialPhaseDuration = POLLING_INITIAL_PHASE_DURATION_MS;
const initialPollInterval = POLLING_INITIAL_INTERVAL_MS;
const finalPollInterval = POLLING_FINAL_INTERVAL_MS;
const startTime = Date.now();

const poll = async () => {
Expand Down Expand Up @@ -575,10 +593,10 @@ const ProjectPage = () => {
await navigator.clipboard.writeText(prompt);
setCopiedPrompts(prev => ({ ...prev, [messageId]: true }));

// Reset the copied state after 2 seconds
// Reset the copied state after delay
setTimeout(() => {
setCopiedPrompts(prev => ({ ...prev, [messageId]: false }));
}, 2000);
}, COPY_FEEDBACK_RESET_DELAY_MS);
} catch (error) {
console.error('Failed to copy prompt:', error);
}
Expand Down Expand Up @@ -886,7 +904,7 @@ const ProjectPage = () => {
// Check if mobile on mount and resize
useEffect(() => {
const checkMobile = () => {
setIsMobile(window.innerWidth < 768);
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT_PX);
};

checkMobile();
Expand All @@ -905,7 +923,7 @@ const ProjectPage = () => {
const totalCharacters = calculateTotalCharacters(messages);
const existingPromptCount = messages.filter(m => m.type === 'assistant' && (m as any).prompt).length;
setIsCharacterLimitReached(totalCharacters >= MAX_CHARACTERS);
setIsPromptLimitReached(existingPromptCount >= 3);
setIsPromptLimitReached(existingPromptCount >= MAX_PROMPTS_PER_CHAT);
}, [messages]);


Expand All @@ -918,8 +936,8 @@ const ProjectPage = () => {
const containerWidth = containerRect.width;
const newLeftWidth = (newX / containerWidth) * 100;

// Constrain between 25% and 75%
const constrainedWidth = Math.min(75, Math.max(25, newLeftWidth));
// Constrain between min and max panel width
const constrainedWidth = Math.min(PANEL_MAX_WIDTH_PERCENT, Math.max(PANEL_MIN_WIDTH_PERCENT, newLeftWidth));
setLeftPanelWidth(constrainedWidth);
}
};
Expand Down Expand Up @@ -949,14 +967,12 @@ const ProjectPage = () => {
const textarea = e.target;
textarea.style.height = 'auto';
const scrollHeight = textarea.scrollHeight;
const maxHeight = 180;

if (scrollHeight <= maxHeight) {
if (scrollHeight <= TEXTAREA_MAX_HEIGHT_PX) {
textarea.style.height = scrollHeight + 'px';
setTextareaHeight(scrollHeight + 'px');
} else {
textarea.style.height = maxHeight + 'px';
setTextareaHeight(maxHeight + 'px');
textarea.style.height = TEXTAREA_MAX_HEIGHT_PX + 'px';
setTextareaHeight(TEXTAREA_MAX_HEIGHT_PX + 'px');
}
};

Expand Down Expand Up @@ -994,8 +1010,8 @@ const ProjectPage = () => {

// Update chat title in local state if this is the first user message
if (saveResult.success && messages.length === 0) {
const title = currentInput.length > 20
? currentInput.substring(0, 20) + '...'
const title = currentInput.length > CHAT_TITLE_PREVIEW_LENGTH
? currentInput.substring(0, CHAT_TITLE_PREVIEW_LENGTH) + '...'
: currentInput;

setProjectChats(prevChats =>
Expand Down
17 changes: 12 additions & 5 deletions src/app/project/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,13 @@ import { useEffect, useState } from "react"
import { submitFeedback } from "../../../actions/feedback"
import Nav from "@/components/core/Nav"
import GithubOAuthDeprecatedNotice from "@/components/GithubOAuthDeprecatedNotice"
import {
SWR_DEDUPING_INTERVAL_MS,
SWR_REFRESH_INTERVAL_MS,
SWR_ERROR_RETRY_COUNT,
SWR_ERROR_RETRY_INTERVAL_MS,
FEEDBACK_SUCCESS_CLOSE_DELAY_MS,
} from '@/constants'

interface Project {
id: string;
Expand Down Expand Up @@ -74,10 +81,10 @@ export default function ProjectsPage() {
{
revalidateOnFocus: false, // Don't refetch when window gains focus
revalidateOnReconnect: true, // Refetch when reconnecting to the internet
dedupingInterval: 60000, // Dedupe requests within 1 minute
refreshInterval: 5 * 60 * 1000, // Refresh every 5 minutes
errorRetryCount: 3, // Retry on error 3 times
errorRetryInterval: 1000, // Wait 1 second between retries
dedupingInterval: SWR_DEDUPING_INTERVAL_MS,
refreshInterval: SWR_REFRESH_INTERVAL_MS,
errorRetryCount: SWR_ERROR_RETRY_COUNT,
errorRetryInterval: SWR_ERROR_RETRY_INTERVAL_MS,
}
);

Expand All @@ -102,7 +109,7 @@ export default function ProjectsPage() {
setTimeout(() => {
setIsFeedbackOpen(false);
setFeedbackMessage(null);
}, 2000);
}, FEEDBACK_SUCCESS_CLOSE_DELAY_MS);
} else {
setFeedbackMessage({
type: 'error',
Expand Down
Loading