Skip to content

Commit c73d364

Browse files
committed
feat: added db operations for creating chat and messages
1 parent caa072c commit c73d364

5 files changed

Lines changed: 235 additions & 61 deletions

File tree

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
/*
2+
Warnings:
3+
4+
- A unique constraint covering the columns `[userId]` on the table `Subscription` will be added. If there are existing duplicate values, this will fail.
5+
- Changed the type of `role` on the `Message` table. No cast exists, the column would be dropped and recreated, which cannot be done if there is data, since the column is required.
6+
7+
*/
8+
-- CreateEnum
9+
CREATE TYPE "MessageRole" AS ENUM ('USER', 'ASSISTANT');
10+
11+
-- AlterTable
12+
ALTER TABLE "Message" DROP COLUMN "role",
13+
ADD COLUMN "role" "MessageRole" NOT NULL;
14+
15+
-- CreateIndex
16+
CREATE UNIQUE INDEX "Subscription_userId_key" ON "Subscription"("userId");

prisma/schema.prisma

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ model Message {
102102
chatId String
103103
chat Chat @relation(fields: [chatId], references: [id], onDelete: Cascade)
104104
content String
105-
role String
105+
role MessageRole
106106
createdAt DateTime @default(now())
107107
updatedAt DateTime @updatedAt
108108
@@index([chatId])
@@ -125,4 +125,9 @@ enum TransactionStatus {
125125
PENDING
126126
SUCCESS
127127
FAILED
128+
}
129+
130+
enum MessageRole {
131+
USER
132+
ASSISTANT
128133
}

src/app/api/ask/route.ts

Lines changed: 56 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { env } from "@/env";
22
import { auth } from "@/server/auth";
33
import { fetchChatCompletion } from "@/models/service";
44
import { DEFAULT_MODEL_ID, getModelById } from "@/models/constants";
5-
5+
import { db } from "@/server/db";
66
interface ChatMessage {
77
role: "user" | "assistant" | "system";
88
content: string;
@@ -11,6 +11,7 @@ interface ChatMessage {
1111
interface TypeGPTPayload {
1212
messages: ChatMessage[];
1313
model?: string;
14+
chatId: string;
1415
}
1516

1617
interface TypeGPTErrorResponse {
@@ -30,9 +31,18 @@ export async function POST(req: Request): Promise<Response> {
3031
return new Response("Unauthorized", { status: 401 });
3132
}
3233

33-
const { messages, model = DEFAULT_MODEL_ID } =
34+
const { messages, model = DEFAULT_MODEL_ID, chatId } =
3435
(await req.json()) as TypeGPTPayload;
3536

37+
// Save user message
38+
await db.message.create({
39+
data: {
40+
chatId,
41+
content: messages[messages.length - 1]?.content ?? "",
42+
role: "USER",
43+
},
44+
});
45+
3646
const modelInfo = getModelById(model);
3747
if (!modelInfo) {
3848
return new Response(
@@ -50,6 +60,8 @@ export async function POST(req: Request): Promise<Response> {
5060
const writer = writable.getWriter();
5161

5262
void (async () => {
63+
let accumulatedContent = "";
64+
5365
try {
5466
const response = await fetchChatCompletion({
5567
modelId: model,
@@ -89,11 +101,53 @@ export async function POST(req: Request): Promise<Response> {
89101
const { done, value } = await reader.read();
90102

91103
if (done) {
104+
// Save AI message to database after stream completes
105+
if (accumulatedContent.trim()) {
106+
await db.message.create({
107+
data: {
108+
chatId,
109+
content: accumulatedContent,
110+
role: "ASSISTANT",
111+
},
112+
});
113+
}
114+
92115
await writer.write(encoder.encode("data: [DONE]\n\n"));
93116
await writer.close();
94117
break;
95118
}
96119

120+
// Accumulate content for database storage
121+
const chunk = new TextDecoder().decode(value);
122+
const lines = chunk.split("\n");
123+
124+
for (const line of lines) {
125+
if (line.trim() === "") continue;
126+
127+
if (line.startsWith("data: ")) {
128+
const data = line.substring(6);
129+
130+
if (data === "[DONE]") continue;
131+
132+
try {
133+
const parsedData = JSON.parse(data) as {
134+
choices?: Array<{
135+
delta?: {
136+
content?: string;
137+
};
138+
}>;
139+
};
140+
141+
const content = parsedData.choices?.[0]?.delta?.content;
142+
if (content) {
143+
accumulatedContent += content;
144+
}
145+
} catch (e) {
146+
// Ignore parsing errors for non-JSON lines
147+
}
148+
}
149+
}
150+
97151
await writer.write(value);
98152
}
99153
} catch (error) {
@@ -112,7 +166,6 @@ export async function POST(req: Request): Promise<Response> {
112166
}
113167
})();
114168

115-
116169
return new Response(readable, { headers });
117170
} catch (error) {
118171
console.error("Error in chat API:", error);

src/components/ui/ui-input.tsx

Lines changed: 34 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,13 @@ import React, { useState, useRef, useEffect } from "react";
33
import { useSession } from "next-auth/react";
44
import { Textarea } from "@/components/ui/textarea";
55
import { Button } from "@/components/ui/button";
6-
import { Badge } from "@/components/ui/badge";
76
import {
87
ChatCircleDotsIcon,
98
MicrophoneIcon,
109
SpinnerGapIcon,
1110
} from "@phosphor-icons/react";
12-
import {
13-
Select,
14-
SelectContent,
15-
SelectGroup,
16-
SelectItem,
17-
SelectLabel,
18-
SelectTrigger,
19-
SelectValue,
20-
} from "@/components/ui/select";
2111
import ReactMarkdown from "react-markdown";
2212
import SyntaxHighlighter from "react-syntax-highlighter";
23-
import { vs2015 } from "react-syntax-highlighter/dist/esm/styles/hljs";
2413
import remarkGfm from "remark-gfm";
2514
import { Geist_Mono } from "next/font/google";
2615
import { cn } from "@/lib/utils";
@@ -61,7 +50,7 @@ const UIInput = () => {
6150
scrollToBottom();
6251
}, [messages]);
6352

64-
const saveChat = api.chat.createChat.useMutation({
53+
const createChat = api.chat.createChat.useMutation({
6554
onError: (error) => {
6655
console.error("Error saving chat:", error);
6756
},
@@ -74,6 +63,8 @@ const UIInput = () => {
7463
return;
7564
}
7665

66+
const tempMessageId = `ai-${Date.now()}`;
67+
7768
try {
7869
const reader = response.body?.getReader();
7970
if (!reader) {
@@ -82,8 +73,6 @@ const UIInput = () => {
8273
return;
8374
}
8475

85-
const tempMessageId = `ai-${Date.now()}`;
86-
8776
setMessages((prev) => [
8877
...prev,
8978
{ id: tempMessageId, role: "assistant", content: "" },
@@ -111,6 +100,7 @@ const UIInput = () => {
111100
const { done, value } = await reader.read();
112101

113102
if (done) {
103+
// Final update with complete content
114104
setMessages((prev) =>
115105
prev.map((msg) =>
116106
msg.id === tempMessageId
@@ -152,8 +142,22 @@ const UIInput = () => {
152142
content?: string;
153143
};
154144
}>;
145+
error?: string;
155146
};
156147

148+
// Handle error responses
149+
if (parsedData.error) {
150+
console.error("Stream error:", parsedData.error);
151+
setMessages((prev) =>
152+
prev.map((msg) =>
153+
msg.id === tempMessageId
154+
? { ...msg, content: `Error: ${parsedData.error}` }
155+
: msg,
156+
),
157+
);
158+
break;
159+
}
160+
157161
const content = parsedData.choices?.[0]?.delta?.content;
158162
if (content) {
159163
accumulatedContent += content;
@@ -169,13 +173,16 @@ const UIInput = () => {
169173
updateMessage(accumulatedContent);
170174
}
171175
}
172-
173-
saveChat.mutate({
174-
message: userMessage,
175-
model: model,
176-
});
177176
} catch (error) {
178177
console.error("Error processing stream:", error);
178+
// Update message with error state
179+
setMessages((prev) =>
180+
prev.map((msg) =>
181+
msg.id === tempMessageId
182+
? { ...msg, content: "Error: Failed to process response" }
183+
: msg,
184+
),
185+
);
179186
} finally {
180187
setIsLoading(false);
181188
abortControllerRef.current = null;
@@ -209,6 +216,7 @@ const UIInput = () => {
209216
abortControllerRef.current = new AbortController();
210217

211218
try {
219+
const {chatId} = await createChat.mutateAsync();
212220
setTimeout(() => {
213221
void (async () => {
214222
try {
@@ -220,6 +228,7 @@ const UIInput = () => {
220228
body: JSON.stringify({
221229
messages: [{ role: "user", content: currentQuery }],
222230
model: model,
231+
chatId: chatId,
223232
}),
224233
signal: abortControllerRef.current?.signal,
225234
});
@@ -395,6 +404,12 @@ const UIInput = () => {
395404
<Textarea
396405
value={query}
397406
onChange={(e) => setQuery(e.target.value)}
407+
onKeyDown={(e) => {
408+
if (e.key === "Enter" && !e.shiftKey) {
409+
e.preventDefault();
410+
void handleCreateChat(e as any);
411+
}
412+
}}
398413
placeholder="Ask whatever you want to be"
399414
className="h-[2rem] resize-none rounded-none border-none bg-transparent px-0 py-1 shadow-none ring-0 focus-visible:ring-0 dark:bg-transparent"
400415
disabled={isLoading}

0 commit comments

Comments
 (0)