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
43 changes: 35 additions & 8 deletions frontend/src/components/ChatBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,17 @@ function TokenWarning({
currentInput,
chatId,
className,
billingStatus
billingStatus,
onCompress,
isCompressing = false
}: {
messages: ChatMessage[];
currentInput: string;
chatId?: string;
className?: string;
billingStatus?: BillingStatus;
onCompress?: () => void;
isCompressing?: boolean;
}) {
const totalTokens =
messages.reduce((acc, msg) => acc + estimateTokenCount(msg.content), 0) +
Expand All @@ -37,7 +41,7 @@ function TokenWarning({
const navigate = useNavigate();

// Check if user is on starter plan
const isStarter = billingStatus?.product_name.toLowerCase().includes("starter") || false;
const isStarter = billingStatus?.product_name?.toLowerCase().includes("starter") || false;

// Token thresholds for different plan types
const STARTER_WARNING_THRESHOLD = 4000;
Expand All @@ -60,6 +64,19 @@ function TokenWarning({
}
};

// Determine button text based on compression state
const getButtonText = () => {
if (isCompressing) {
return { desktop: "Compressing...", mobile: "Compressing..." };
}
if (onCompress) {
return { desktop: "Compress chat", mobile: "Compress" };
}
return { desktop: "Start a new chat", mobile: "New chat" };
};

const buttonText = getButtonText();

return (
<div
className={cn(
Expand All @@ -71,15 +88,19 @@ function TokenWarning({
>
<div className="flex items-center gap-2 min-w-0">
<span className="text-[11px] font-semibold text-foreground/70 shrink-0">Tip:</span>
<span className="min-w-0">Long chats cause you to reach your usage limits faster.</span>
<span className="min-w-0">This chat is getting long. Compress it to save tokens.</span>
</div>
{chatId && (
<button
onClick={handleNewChat}
className="font-medium text-primary hover:text-primary/80 hover:underline transition-colors whitespace-nowrap shrink-0 ml-4"
onClick={!isCompressing ? onCompress || handleNewChat : undefined}
disabled={isCompressing}
Comment thread
AnthonyRonning marked this conversation as resolved.
className={cn(
"font-medium text-primary transition-colors whitespace-nowrap shrink-0 ml-4",
isCompressing ? "opacity-70 cursor-default" : "hover:text-primary/80 hover:underline"
)}
>
<span className="hidden md:inline">Start a new chat</span>
<span className="md:hidden">New chat</span>
<span className="hidden md:inline">{buttonText.desktop}</span>
<span className="md:hidden">{buttonText.mobile}</span>
<span className="sr-only">, to reduce token usage</span>
</button>
)}
Expand All @@ -91,12 +112,16 @@ export default function Component({
onSubmit,
startTall,
messages = [],
isStreaming = false
isStreaming = false,
onCompress,
isSummarizing = false
}: {
onSubmit: (input: string) => void;
startTall?: boolean;
messages?: ChatMessage[];
isStreaming?: boolean;
onCompress?: () => void;
isSummarizing?: boolean;
}) {
const [inputValue, setInputValue] = useState("");
const {
Expand Down Expand Up @@ -271,6 +296,8 @@ export default function Component({
currentInput={inputValue}
chatId={chatId}
billingStatus={freshBillingStatus}
onCompress={onCompress}
isCompressing={isSummarizing}
/>
<form
className={cn(
Expand Down
124 changes: 120 additions & 4 deletions frontend/src/routes/_auth.chat.$chatId.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { useQuery, useQueryClient } from "@tanstack/react-query";
import { InfoPopover } from "@/components/InfoPopover";
import { Button } from "@/components/ui/button";
import { BillingStatus } from "@/billing/billingApi";
import { useNavigate } from "@tanstack/react-router";

export const Route = createFileRoute("/_auth/chat/$chatId")({
component: ChatComponent
Expand Down Expand Up @@ -70,12 +71,14 @@ function SystemMessage({ text, loading }: { text: string; loading?: boolean }) {

function ChatComponent() {
const { chatId } = Route.useParams();
const { model, persistChat, getChatById, userPrompt, setUserPrompt } = useLocalState();
const { model, persistChat, getChatById, userPrompt, setUserPrompt, addChat } = useLocalState();
const openai = useOpenAI();
const queryClient = useQueryClient();
const [showScrollButton, setShowScrollButton] = useState(false);
const navigate = useNavigate();

const [error, setError] = useState("");
const [isSummarizing, setIsSummarizing] = useState(false);

const chatContainerRef = useRef<HTMLDivElement>(null);

Expand Down Expand Up @@ -167,12 +170,19 @@ function ChatComponent() {
const userPromptEffectRan = useRef(false);

useEffect(() => {
// Make sure we don't run this more than once per mount
if (userPromptEffectRan.current) return;
userPromptEffectRan.current = true;

// Check if we have a user prompt to send
if (userPrompt) {
console.log("User prompt found, sending to chat");
console.log("User prompt found for chatId:", chatId, "sending to chat");
console.log("USER PROMPT:", userPrompt);
sendMessage(userPrompt);

// Set a small delay to ensure all state is properly initialized
setTimeout(() => {
sendMessage(userPrompt);
}, 100);
}
Comment thread
AnthonyRonning marked this conversation as resolved.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
Expand Down Expand Up @@ -421,6 +431,106 @@ function ChatComponent() {
[localChat, model, openai, persistChat, queryClient, setUserPrompt, chatId]
);

// Chat compression function
const compressChat = useCallback(async () => {
try {
setIsSummarizing(true);

// 1. Build summarization prompt with detailed instructions
const summarizerSystem = `You are "Summarizer-v1", an expert summarization assistant.

TASK
• First, write a concise paragraph (3–5 sentences) summarizing the overall conversation.
• Then, produce 10–20 markdown bullet points, each ≤ 30 words.
• Break complex ideas into multiple bullets for clarity.

CONTENT TO CAPTURE
1. **Key Information** – essential facts, data points, and statements.
2. **Decisions and Conclusions** – final choices or outcomes.
3. **Open Questions and Action Items** – unresolved issues or tasks to be completed.
4. **User Preferences and Priorities** – expressed likes, dislikes, or priorities.

STYLE & RULES
• Do **not** mention the assistant, the user, message counts, or dates.
• Do **not** quote anyone verbatim; paraphrase.
• Avoid passive voice; start each bullet with a strong noun or verb.
• Use present tense where possible ("Decide to migrate…", "User prefers…").
• No headings or extra text—*just* the paragraph summary followed by the bullet list.

END OF INSTRUCTIONS`;

const summarizationMessages = [
{ role: "system" as const, content: summarizerSystem },
...localChat.messages
];

// 2. Stream the summary
let summary = "";
const stream = openai.beta.chat.completions.stream({
model,
messages: summarizationMessages,
temperature: 0.3,
max_tokens: 600,
stream: true
});

for await (const chunk of stream) {
summary += chunk.choices[0]?.delta?.content ?? "";
}
await stream.finalChatCompletion();

// 3. Build initial message for the new chat
const initialMsg =
`Below is the summary of our previous chats:\n\n${summary}\n\n` +
`I will follow up with additional conversations based on our previous chat summary`;

// Try a completely different approach - work directly with storage

// 1. First create a new chat with the title directly inherited from the original chat
console.log("Creating new chat with summary from original chat");
const inheritedTitle = localChat.title; // Use the exact same title as the original chat
const id = await addChat(inheritedTitle);

// 2. Completely reset user prompt
setUserPrompt("");

// 3. Take the direct storage approach instead of relying on React state/effects
// Create a fake user message directly in storage that the next page will read
const initialChatData: Chat = {
id: id,
title: inheritedTitle,
messages: [{ role: "user" as const, content: initialMsg }]
};

// Explicitly persist this chat with the initial message directly
await persistChat(initialChatData);

// 4. Force refetch of both chat data and history list when navigating
queryClient.invalidateQueries({
queryKey: ["chat", id],
refetchType: "all"
});

// Make sure history list is also invalidated to show the new chat
queryClient.invalidateQueries({
queryKey: ["chatHistory"],
refetchType: "all"
});

// 5. Reset the flag for good measure
userPromptEffectRan.current = false;
Comment thread
AnthonyRonning marked this conversation as resolved.

// 6. Navigate to the new chat which should now have the initial message
console.log("Navigating to new chat with pre-persisted message:", id);
navigate({ to: "/chat/$chatId", params: { chatId: id } });
} catch (e) {
console.error("compressChat failed:", e);
setError("Could not compress chat – please try again.");
} finally {
setIsSummarizing(false);
}
}, [localChat, model, openai, addChat, navigate, setUserPrompt]);

return (
<div className="grid h-dvh w-full grid-cols-1 md:grid-cols-[280px_1fr]">
<Sidebar chatId={chatId} isOpen={isSidebarOpen} onToggle={toggleSidebar} />
Expand Down Expand Up @@ -471,7 +581,13 @@ function ChatComponent() {
{/* Place the chat box inline (below messages) in normal flow */}
<div className="w-full max-w-[45rem] mx-auto flex flex-col gap-2 px-2 pb-2">
{error && <AlertDestructive title="Error" description={error} />}
<ChatBox onSubmit={sendMessage} messages={localChat.messages} isStreaming={isLoading} />
<ChatBox
onSubmit={sendMessage}
messages={localChat.messages}
isStreaming={isLoading || isSummarizing}
onCompress={compressChat}
isSummarizing={isSummarizing}
/>
</div>
</main>
</div>
Expand Down
Loading