Skip to content

Commit 7b2677c

Browse files
committed
feat: smooth chat creation and redirection
1 parent 819d1e9 commit 7b2677c

5 files changed

Lines changed: 55 additions & 66 deletions

File tree

src/app/(app)/settings/subscription/page.tsx

Lines changed: 1 addition & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ import { History } from "@/components/subscription/history";
2020

2121
export default async function SubscriptionPage() {
2222
const user = await FetchUser();
23-
console.log(user);
2423
return (
2524
<div className="bg-background text-foreground min-h-screen w-full border">
2625
<div className="mx-auto w-full max-w-6xl p-8">
@@ -52,20 +51,6 @@ export default async function SubscriptionPage() {
5251
about={user?.about as string}
5352
plan={user?.subscription?.plan as string}
5453
/>
55-
56-
<div className="space-y-1 pt-8">
57-
<h2 className="text-xl font-bold text-white">Danger Zone</h2>
58-
<p className="text-muted-foreground text-sm">
59-
Permanently delete your account and all associated data.
60-
</p>
61-
<Button
62-
variant="destructive"
63-
className="mt-4 rounded-lg bg-red-600 px-6 py-2 font-medium text-white hover:bg-red-700"
64-
>
65-
Delete Account
66-
</Button>
67-
</div>
68-
6954
</div>
7055

7156
{/* Right Column - Tabs Section */}
@@ -80,10 +65,7 @@ export default async function SubscriptionPage() {
8065
>
8166
Account
8267
</TabsTrigger>
83-
<TabsTrigger className="rounded-lg text-xs" value="history">
84-
History & Sync
85-
</TabsTrigger>
86-
<TabsTrigger className="rounded-lg text-xs" value="models">
68+
<TabsTrigger className="rounded-lg text-xs" value="models">
8769
Models
8870
</TabsTrigger>
8971
<TabsTrigger className="rounded-lg text-xs" value="api-keys">
@@ -99,9 +81,6 @@ export default async function SubscriptionPage() {
9981
<TabsContent value="account">
10082
<Customisation />
10183
</TabsContent>
102-
<TabsContent value="history">
103-
<History />
104-
</TabsContent>
10584
<TabsContent value="models">
10685
<Models />
10786
</TabsContent>

src/app/_components/Chat.tsx

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ function handleStopListening() {
3434

3535
const Chat = ({ chatId: initialChatId }: { chatId: string }) => {
3636
const { modelId: model, setModelId: setModel } = useModel();
37-
const [messages, setMessages] = useState<ChatMessage[]>([]);
37+
const [localMessages, setMessages] = useState<ChatMessage[]>([]);
3838
const [search, setSearch] = useState(false);
3939
const [input, setInput] = useState(() => {
4040
if (typeof window === "undefined") return "";
@@ -69,24 +69,28 @@ const Chat = ({ chatId: initialChatId }: { chatId: string }) => {
6969
onScroll: scrollToBottom,
7070
});
7171

72-
const { data: chatMessages } = api.chat.getChatMessages.useQuery({ chatId });
72+
const { data: chatMessages, isLoading: isQueryLoading } = api.chat.getChatMessages.useQuery({ chatId });
73+
74+
// Derive display messages: prefer local state (during/after streaming),
75+
// fall back to server data so cached queries render instantly without a loader flash
76+
const messages = localMessages.length > 0
77+
? localMessages
78+
: (chatMessages?.messages.map((m) => ({
79+
id: m.id,
80+
role: m.role === "USER" ? ("user" as const) : ("assistant" as const),
81+
content: m.content,
82+
})) ?? []);
7383

7484
useEffect(() => {
7585
setChatId(initialChatId);
86+
setMessages([]);
7687
}, [initialChatId]);
7788

7889
useEffect(() => {
79-
if (chatMessages) {
80-
setMessages(
81-
chatMessages.messages.map((message) => ({
82-
id: message.id,
83-
role: message.role === "USER" ? ("user" as const) : ("assistant" as const),
84-
content: message.content,
85-
})),
86-
);
90+
if (chatMessages && localMessages.length === 0) {
8791
requestAnimationFrame(scrollToBottom);
8892
}
89-
}, [chatMessages, scrollToBottom]);
93+
}, [chatMessages, localMessages.length, scrollToBottom]);
9094

9195
const toggleWrap = useCallback(() => setIsWrapped((prev) => !prev), []);
9296

@@ -208,7 +212,7 @@ const Chat = ({ chatId: initialChatId }: { chatId: string }) => {
208212
<div className="relative flex h-full w-full flex-col">
209213
<div className="no-scrollbar flex flex-1 flex-col overflow-y-auto px-4 pb-40 md:px-4">
210214
<div className="mx-auto w-full max-w-4xl py-4">
211-
{messages.length === 0 ? (
215+
{messages.length === 0 && isQueryLoading ? (
212216
<div className="text-muted-foreground flex h-[50vh] items-center justify-center">
213217
<Loader2Icon className="animate-spin" />
214218
</div>

src/app/api/ask/route.ts

Lines changed: 33 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -53,21 +53,6 @@ export async function POST(req: NextRequest): Promise<Response> {
5353
);
5454
}
5555

56-
// Save user message
57-
await db.message.create({
58-
data: {
59-
chatId,
60-
content: userContent,
61-
role: "USER",
62-
},
63-
});
64-
if (!chat.title && userContent) {
65-
await db.chat.update({
66-
where: { id: chatId },
67-
data: { title: userContent.slice(0, 100) },
68-
});
69-
}
70-
7156
const modelInfo = getModelById(model);
7257
if (!modelInfo) {
7358
return new Response(
@@ -81,7 +66,23 @@ export async function POST(req: NextRequest): Promise<Response> {
8166
? req.cookies.get(`${COOKIE_PREFIX}${modelInfo.provider}`)?.value
8267
: undefined;
8368

84-
// Call the model FIRST so we can return a proper HTTP error if it fails
69+
// Start DB writes in parallel with the LLM call to reduce time-to-first-byte.
70+
// The LLM reads messages from the request body, not the DB, so this is safe.
71+
const saveUserMessage = db.message.create({
72+
data: {
73+
chatId,
74+
content: userContent,
75+
role: "USER",
76+
},
77+
});
78+
// Title update is non-critical — fire and forget
79+
if (!chat.title && userContent) {
80+
void db.chat.update({
81+
where: { id: chatId },
82+
data: { title: userContent.slice(0, 100) },
83+
});
84+
}
85+
8586
let modelResponse: Response;
8687
try {
8788
modelResponse = await fetchChatCompletion({
@@ -101,6 +102,9 @@ export async function POST(req: NextRequest): Promise<Response> {
101102
);
102103
}
103104

105+
// Ensure user message is persisted before we start streaming
106+
await saveUserMessage;
107+
104108
if (!modelResponse.ok) {
105109
// Forward the upstream error status and body to the client
106110
let errorBody: ChatErrorResponse = {};
@@ -155,18 +159,19 @@ export async function POST(req: NextRequest): Promise<Response> {
155159
if (done) {
156160
// Save AI message to database after stream completes
157161
if (accumulatedContent.trim()) {
158-
await db.message.create({
159-
data: {
160-
chatId,
161-
content: accumulatedContent,
162-
role: "ASSISTANT",
163-
},
164-
});
165-
// Update chat timestamp so sidebar ordering stays correct
166-
await db.chat.update({
167-
where: { id: chatId },
168-
data: { updatedAt: new Date() },
169-
});
162+
await Promise.all([
163+
db.message.create({
164+
data: {
165+
chatId,
166+
content: accumulatedContent,
167+
role: "ASSISTANT",
168+
},
169+
}),
170+
db.chat.update({
171+
where: { id: chatId },
172+
data: { updatedAt: new Date() },
173+
}),
174+
]);
170175
}
171176

172177
await writer.write(encoder.encode("data: [DONE]\n\n"));

src/components/ui/ui-input.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import TabsSuggestion from "@/components/ui/tabs-suggestion";
1010
import { ModelSelector } from "@/components/ui/model-selector";
1111
import { useModel } from "@/hooks/use-model";
1212
import { useStreamChat } from "@/hooks/use-stream-chat";
13-
import { useRouter } from "next/navigation";
1413
import SpeechRecognition, { useSpeechRecognition } from "react-speech-recognition";
1514
import { useSpeechSynthesis } from "react-speech-kit";
1615
import { toast } from "sonner";
@@ -32,7 +31,6 @@ function handleStopListening() {
3231

3332
const UIInput = () => {
3433
const session = useSession();
35-
const router = useRouter();
3634
const { modelId: model, setModelId: setModel } = useModel();
3735
const [modeOfChatting, setModeOfChatting] = useState<"text" | "voice">("text");
3836
const [query, setQuery] = useState("");
@@ -113,6 +111,8 @@ const UIInput = () => {
113111
const result = await createChat.mutateAsync();
114112
chatId = result.chatId!;
115113
currentChatIdRef.current = chatId;
114+
// Update URL immediately without triggering navigation or component remount
115+
window.history.replaceState(null, "", `/ask/${chatId}`);
116116
}
117117

118118
const allMessages = [...messages, userMessage];
@@ -124,7 +124,6 @@ const UIInput = () => {
124124
if (modeOfChatting === "voice" && ttsSupported && selectedVoice && content) {
125125
speak({ text: content, voice: selectedVoice });
126126
}
127-
router.push(`/ask/${chatId}`);
128127
} catch (error) {
129128
if ((error as Error).name !== "AbortError") console.error("Error:", error);
130129
setIsLoading(false);

src/styles/globals.css

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -310,10 +310,12 @@ html.boring-theme.dark {
310310

311311
::selection {
312312
background-color: var(--primary);
313+
color: var(--primary-foreground);
313314
}
314315

315316
.dark::selection {
316-
background-color: var(--accent);
317+
background-color: var(--primary);
318+
color: var(--primary-foreground);
317319
}
318320

319321
@media (prefers-reduced-motion: reduce) {

0 commit comments

Comments
 (0)