refactor: update navigation and chat initialization logic

- Changed navigation from researcher to new-chat in EditorPage.
- Updated NewChatPage to support lazy thread creation on first message.
- Adjusted breadcrumb to not display thread ID for new-chat sections.
- Improved user experience by modifying error messages and loading states.
- Enhanced styling in model-selector component for better visibility.
This commit is contained in:
Anish Sarkar 2025-12-25 12:18:45 +05:30
parent 85ddaedd9e
commit bf22156664
5 changed files with 53 additions and 35 deletions

View file

@ -323,7 +323,7 @@ export default function EditorPage() {
if (hasUnsavedChanges) {
setShowUnsavedDialog(true);
} else {
router.push(`/dashboard/${searchSpaceId}/researcher`);
router.push(`/dashboard/${searchSpaceId}/new-chat`);
}
};
@ -333,12 +333,12 @@ export default function EditorPage() {
setGlobalHasUnsavedChanges(false);
setHasUnsavedChanges(false);
// If there's a pending navigation (from sidebar), use that; otherwise go back to researcher
// If there's a pending navigation (from sidebar), use that; otherwise go back to chat
if (pendingNavigation) {
router.push(pendingNavigation);
setPendingNavigation(null);
} else {
router.push(`/dashboard/${searchSpaceId}/researcher`);
router.push(`/dashboard/${searchSpaceId}/new-chat`);
}
};
@ -379,7 +379,7 @@ export default function EditorPage() {
</CardHeader>
<CardContent>
<Button
onClick={() => router.push(`/dashboard/${searchSpaceId}/researcher`)}
onClick={() => router.push(`/dashboard/${searchSpaceId}/new-chat`)}
variant="outline"
className="gap-2"
>

View file

@ -7,7 +7,7 @@ import {
useExternalStoreRuntime,
} from "@assistant-ui/react";
import { useAtomValue, useSetAtom } from "jotai";
import { useParams, useRouter } from "next/navigation";
import { useParams } from "next/navigation";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { toast } from "sonner";
import {
@ -126,7 +126,6 @@ interface ThinkingStepData {
export default function NewChatPage() {
const params = useParams();
const router = useRouter();
const [isInitializing, setIsInitializing] = useState(true);
const [threadId, setThreadId] = useState<number | null>(null);
const [messages, setMessages] = useState<ThreadMessageLike[]>([]);
@ -168,6 +167,7 @@ export default function NewChatPage() {
}, [params.chat_id]);
// Initialize thread and load messages
// For new chats (no urlChatId), we use lazy creation - thread is created on first message
const initializeThread = useCallback(async () => {
setIsInitializing(true);
@ -206,22 +206,20 @@ export default function NewChatPage() {
setMessageDocumentsMap(restoredDocsMap);
}
}
} else {
// Create new thread
const newThread = await createThread(searchSpaceId, "New Chat");
setThreadId(newThread.id);
router.replace(`/dashboard/${searchSpaceId}/new-chat/${newThread.id}`);
}
// For new chats (urlChatId === 0), don't create thread yet
// Thread will be created lazily when user sends first message
// This improves UX (instant load) and avoids orphan threads
} catch (error) {
console.error("[NewChatPage] Failed to initialize thread:", error);
// Keep threadId as null - don't use Date.now() as it creates an invalid ID
// that will cause 404 errors on subsequent API calls
setThreadId(null);
toast.error("Failed to initialize chat. Please try again.");
toast.error("Failed to load chat. Please try again.");
} finally {
setIsInitializing(false);
}
}, [urlChatId, searchSpaceId, router, setMessageDocumentsMap]);
}, [urlChatId, setMessageDocumentsMap]);
// Initialize on mount
useEffect(() => {
@ -240,8 +238,6 @@ export default function NewChatPage() {
// Handle new message from user
const onNew = useCallback(
async (message: AppendMessage) => {
if (!threadId) return;
// Extract user query text from content parts
let userQuery = "";
for (const part of message.content) {
@ -273,6 +269,27 @@ export default function NewChatPage() {
return;
}
// Lazy thread creation: create thread on first message if it doesn't exist
let currentThreadId = threadId;
if (!currentThreadId) {
try {
const newThread = await createThread(searchSpaceId, "New Chat");
currentThreadId = newThread.id;
setThreadId(currentThreadId);
// Update URL silently using browser API (not router.replace) to avoid
// interrupting the ongoing fetch/streaming with React navigation
window.history.replaceState(
null,
"",
`/dashboard/${searchSpaceId}/new-chat/${currentThreadId}`
);
} catch (error) {
console.error("[NewChatPage] Failed to create thread:", error);
toast.error("Failed to start chat. Please try again.");
return;
}
}
// Add user message to state
const userMsgId = `msg-user-${Date.now()}`;
const userMessage: ThreadMessageLike = {
@ -311,7 +328,7 @@ export default function NewChatPage() {
},
]
: message.content;
appendMessage(threadId, {
appendMessage(currentThreadId, {
role: "user",
content: persistContent,
}).catch((err) => console.error("Failed to persist user message:", err));
@ -468,7 +485,7 @@ export default function NewChatPage() {
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({
chat_id: threadId,
chat_id: currentThreadId,
user_query: userQuery.trim(),
search_space_id: searchSpaceId,
messages: messageHistory,
@ -601,7 +618,7 @@ export default function NewChatPage() {
// Persist assistant message (with thinking steps for restoration on refresh)
const finalContent = buildContentForPersistence();
if (contentParts.length > 0) {
appendMessage(threadId, {
appendMessage(currentThreadId, {
role: "assistant",
content: finalContent,
}).catch((err) => console.error("Failed to persist assistant message:", err));
@ -678,7 +695,7 @@ export default function NewChatPage() {
},
});
// Show loading state
// Show loading state only when loading an existing thread
if (isInitializing) {
return (
<div className="flex h-[calc(100vh-64px)] items-center justify-center">
@ -687,11 +704,12 @@ export default function NewChatPage() {
);
}
// Show error state if thread initialization failed
if (!threadId) {
// Show error state only if we tried to load an existing thread but failed
// For new chats (urlChatId === 0), threadId being null is expected (lazy creation)
if (!threadId && urlChatId > 0) {
return (
<div className="flex h-[calc(100vh-64px)] flex-col items-center justify-center gap-4">
<div className="text-destructive">Failed to initialize chat</div>
<div className="text-destructive">Failed to load chat</div>
<button
type="button"
onClick={() => {

View file

@ -351,7 +351,7 @@ const getTimeBasedGreeting = (userEmail?: string): string => {
: null;
// Array of greeting variations for each time period
const morningGreetings = ["Good morning", "Rise and shine", "Morning", "Hey there"];
const morningGreetings = ["Good morning", "Fresh start today", "Morning", "Hey there"];
const afternoonGreetings = ["Good afternoon", "Afternoon", "Hey there", "Hi there"];
@ -359,7 +359,7 @@ const getTimeBasedGreeting = (userEmail?: string): string => {
const nightGreetings = ["Good night", "Evening", "Hey there", "Winding down"];
const lateNightGreetings = ["Still up", "Night owl mode", "The night is young", "Hi there"];
const lateNightGreetings = ["Still up", "Night owl mode", "Up past bedtime", "Hi there"];
// Select a random greeting based on time
let greeting: string;
@ -995,7 +995,7 @@ const UserMessage: FC = () => {
>
<UserMessageAttachments />
<div className="aui-user-message-content-wrapper relative col-start-2 min-w-0">
<div className="aui-user-message-content-wrapper col-start-2 min-w-0">
{/* Display mentioned documents as chips */}
{mentionedDocs && mentionedDocs.length > 0 && (
<div className="flex flex-wrap gap-1.5 mb-2 justify-end">
@ -1011,11 +1011,14 @@ const UserMessage: FC = () => {
))}
</div>
)}
<div className="aui-user-message-content wrap-break-word rounded-2xl bg-muted px-4 py-2.5 text-foreground">
<MessagePrimitive.Parts />
</div>
<div className="aui-user-action-bar-wrapper -translate-x-full -translate-y-full absolute top-full left-0 pr-2">
<UserActionBar />
{/* Message bubble with action bar positioned relative to it */}
<div className="relative">
<div className="aui-user-message-content wrap-break-word rounded-2xl bg-muted px-4 py-2.5 text-foreground">
<MessagePrimitive.Parts />
</div>
<div className="aui-user-action-bar-wrapper absolute top-1/2 right-full -translate-y-1/2 pr-1">
<UserActionBar />
</div>
</div>
</div>

View file

@ -165,14 +165,11 @@ export function DashboardBreadcrumb() {
}
// Handle new-chat sub-sections (thread IDs)
// Don't show thread ID in breadcrumb - users identify chats by content, not by ID
if (section === "new-chat") {
breadcrumbs.push({
label: t("chat") || "Chat",
href: `/dashboard/${segments[1]}/new-chat`,
});
if (subSection) {
breadcrumbs.push({ label: `Thread ${subSection}` });
}
return breadcrumbs;
}

View file

@ -175,7 +175,7 @@ export function ModelSelector({ onEdit, onAddNew, className }: ModelSelectorProp
role="combobox"
aria-expanded={open}
className={cn(
"h-9 gap-2 px-3 rounded-xl border border-border/30 bg-background/50 backdrop-blur-sm",
"h-9 gap-2 px-3 rounded-xl border border-border/80 bg-background/50 backdrop-blur-sm",
"hover:bg-muted/80 hover:border-border/30 transition-all duration-200",
"text-sm font-medium text-foreground",
"focus-visible:ring-0 focus-visible:ring-offset-0",