victor HF Staff commited on
Commit
faf1883
·
1 Parent(s): 8cfdcec

thinking UI

Browse files
components/chat-sidebar.tsx CHANGED
@@ -243,7 +243,7 @@ export function ChatSidebar() {
243
  </div>
244
  </div>
245
 
246
- <SidebarGroup className="flex-shrink-0">
247
  <SidebarGroupLabel className={cn(
248
  "px-4 pt-0 text-xs font-medium text-muted-foreground/80 uppercase tracking-wider",
249
  isCollapsed ? "sr-only" : ""
 
243
  </div>
244
  </div>
245
 
246
+ <SidebarGroup className="flex-shrink-0 ">
247
  <SidebarGroupLabel className={cn(
248
  "px-4 pt-0 text-xs font-medium text-muted-foreground/80 uppercase tracking-wider",
249
  isCollapsed ? "sr-only" : ""
components/message.tsx CHANGED
@@ -21,6 +21,109 @@ interface ReasoningMessagePartProps {
21
  isReasoning: boolean;
22
  }
23
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
  export function ReasoningMessagePart({
25
  part,
26
  isReasoning,
@@ -130,8 +233,8 @@ const PurePreviewMessage = ({
130
  const getMessageText = () => {
131
  if (!message.parts) return "";
132
  return message.parts
133
- .filter(part => part.type === "text")
134
- .map(part => (part.type === "text" ? part.text : ""))
135
  .join("\n\n");
136
  };
137
 
@@ -153,57 +256,46 @@ const PurePreviewMessage = ({
153
  )}
154
  >
155
  <div className="flex flex-col w-full space-y-3">
156
- {message.parts?.map((part, i) => {
157
- switch (part.type) {
158
- case "text":
159
- return (
160
- <div
161
- key={`message-${message.id}-part-${i}`}
162
- className="flex flex-row gap-2 items-start w-full"
163
- >
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
164
  <div
165
- className={cn("flex flex-col gap-3 w-full", {
166
- "bg-secondary text-secondary-foreground px-4 py-1.5 rounded-2xl":
167
- message.role === "user",
168
- })}
169
  >
170
- <Markdown>{part.text}</Markdown>
 
 
 
 
 
 
 
171
  </div>
172
- </div>
173
- );
174
- case "tool-invocation":
175
- const { toolName, state, args } = part.toolInvocation;
176
- const result = 'result' in part.toolInvocation ? part.toolInvocation.result : null;
177
-
178
- return (
179
- <ToolInvocation
180
- key={`message-${message.id}-part-${i}`}
181
- toolName={toolName}
182
- state={state}
183
- args={args}
184
- result={result}
185
- isLatestMessage={isLatestMessage}
186
- status={status}
187
- />
188
- );
189
- case "reasoning":
190
- return (
191
- <ReasoningMessagePart
192
- key={`message-${message.id}-${i}`}
193
- // @ts-expect-error part
194
- part={part}
195
- isReasoning={
196
- (message.parts &&
197
- status === "streaming" &&
198
- i === message.parts.length - 1) ??
199
- false
200
- }
201
- />
202
- );
203
- default:
204
  return null;
205
- }
206
- })}
207
  {shouldShowCopyButton && (
208
  <div className="flex justify-start mt-2">
209
  <CopyButton text={getMessageText()} />
 
21
  isReasoning: boolean;
22
  }
23
 
24
+ interface ThinkingPart {
25
+ type: "thinking";
26
+ thinking: string;
27
+ details: Array<{ type: "text"; text: string }>;
28
+ }
29
+
30
+ interface ThinkingMessagePartProps {
31
+ part: ThinkingPart;
32
+ isThinking: boolean;
33
+ }
34
+
35
+ export function ThinkingMessagePart({
36
+ part,
37
+ isThinking,
38
+ }: ThinkingMessagePartProps) {
39
+ const [isExpanded, setIsExpanded] = useState(false);
40
+
41
+ const memoizedSetIsExpanded = useCallback((value: boolean) => {
42
+ setIsExpanded(value);
43
+ }, []);
44
+
45
+ useEffect(() => {
46
+ memoizedSetIsExpanded(isThinking);
47
+ }, [isThinking, memoizedSetIsExpanded]);
48
+
49
+ return (
50
+ <div className="flex flex-col mb-2 group">
51
+ {isThinking ? (
52
+ <div className={cn(
53
+ "flex items-center gap-2.5 rounded-full py-1.5 px-3",
54
+ "bg-primary/10 text-primary",
55
+ "border border-primary/20 w-fit"
56
+ )}>
57
+ <div className="animate-spin h-3.5 w-3.5">
58
+ <SpinnerIcon />
59
+ </div>
60
+ <div className="text-xs font-medium tracking-tight">Thinking...</div>
61
+ </div>
62
+ ) : (
63
+ <button
64
+ onClick={() => setIsExpanded(!isExpanded)}
65
+ className={cn(
66
+ "flex items-center justify-between w-full",
67
+ "rounded-md py-2 px-3 mb-0.5",
68
+ "bg-muted/50 border border-border/60 hover:border-border/80",
69
+ "transition-all duration-150 cursor-pointer",
70
+ isExpanded ? "bg-muted border-primary/20" : ""
71
+ )}
72
+ >
73
+ <div className="flex items-center gap-2.5">
74
+ <div className={cn(
75
+ "flex items-center justify-center w-6 h-6 rounded-full",
76
+ "bg-primary/10",
77
+ "text-primary ring-1 ring-primary/20",
78
+ )}>
79
+ <BrainIcon className="h-3.5 w-3.5" />
80
+ </div>
81
+ <div className="text-sm font-medium text-foreground flex items-center gap-1.5">
82
+ Thinking
83
+ <span className="text-xs text-muted-foreground font-normal">
84
+ (click to {isExpanded ? "hide" : "view"})
85
+ </span>
86
+ </div>
87
+ </div>
88
+ <div className={cn(
89
+ "flex items-center justify-center",
90
+ "rounded-full p-0.5 w-5 h-5",
91
+ "text-muted-foreground hover:text-foreground",
92
+ "bg-background/80 border border-border/50",
93
+ "transition-colors",
94
+ )}>
95
+ {isExpanded ? (
96
+ <ChevronDownIcon className="h-3 w-3" />
97
+ ) : (
98
+ <ChevronUpIcon className="h-3 w-3" />
99
+ )}
100
+ </div>
101
+ </button>
102
+ )}
103
+
104
+ {isExpanded && (
105
+ <div
106
+ className={cn(
107
+ "text-sm text-muted-foreground flex flex-col gap-2",
108
+ "pl-3.5 ml-0.5 mt-1",
109
+ "border-l border-primary/30"
110
+ )}
111
+ >
112
+ {part.details.map((detail, detailIndex) =>
113
+ detail.type === "text" ? (
114
+ <div key={detailIndex} className="px-2 py-1.5 bg-muted/10 rounded-md border border-border/30">
115
+ <Markdown>{detail.text}</Markdown>
116
+ </div>
117
+ ) : (
118
+ "<redacted>"
119
+ ),
120
+ )}
121
+ </div>
122
+ )}
123
+ </div>
124
+ );
125
+ }
126
+
127
  export function ReasoningMessagePart({
128
  part,
129
  isReasoning,
 
233
  const getMessageText = () => {
234
  if (!message.parts) return "";
235
  return message.parts
236
+ .filter((part: { type: string; }) => part.type === "text")
237
+ .map((part: { type: string; text: any; }) => (part.type === "text" ? part.text : ""))
238
  .join("\n\n");
239
  };
240
 
 
256
  )}
257
  >
258
  <div className="flex flex-col w-full space-y-3">
259
+ {message.content &&
260
+ message.content
261
+ .split(/(<think>[\s\S]*?(?:<\/think>|$))/g)
262
+ .map((part: string, i: number) => {
263
+ if (part.startsWith("<think>")) {
264
+ const isClosed = part.endsWith("</think>");
265
+ const thinkingContent = part.slice(
266
+ 7,
267
+ isClosed ? -8 : undefined,
268
+ );
269
+ return (
270
+ <ThinkingMessagePart
271
+ key={`message-${message.id}-part-${i}`}
272
+ part={{
273
+ type: "thinking",
274
+ thinking: thinkingContent,
275
+ details: [{ type: "text", text: thinkingContent }],
276
+ }}
277
+ isThinking={status === "streaming" && !isClosed}
278
+ />
279
+ );
280
+ } else if (part.trim()) {
281
+ return (
282
  <div
283
+ key={`message-${message.id}-part-${i}`}
284
+ className="flex flex-row gap-2 items-start w-full"
 
 
285
  >
286
+ <div
287
+ className={cn("flex flex-col gap-3 w-full", {
288
+ "bg-secondary text-secondary-foreground px-4 py-1.5 rounded-2xl":
289
+ message.role === "user",
290
+ })}
291
+ >
292
+ <Markdown>{part}</Markdown>
293
+ </div>
294
  </div>
295
+ );
296
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
297
  return null;
298
+ })}
 
299
  {shouldShowCopyButton && (
300
  <div className="flex justify-start mt-2">
301
  <CopyButton text={getMessageText()} />
lib/chat-store.ts CHANGED
@@ -1,5 +1,13 @@
1
  import { db } from "./db";
2
- import { chats, messages, type Chat, type Message, MessageRole, type MessagePart, type DBMessage } from "./db/schema";
 
 
 
 
 
 
 
 
3
  import { eq, desc, and } from "drizzle-orm";
4
  import { nanoid } from "nanoid";
5
  import { generateTitle } from "@/app/actions";
@@ -38,28 +46,29 @@ export async function saveMessages({
38
  try {
39
  if (dbMessages.length > 0) {
40
  const chatId = dbMessages[0].chatId;
41
-
42
  // First delete any existing messages for this chat
43
- await db
44
- .delete(messages)
45
- .where(eq(messages.chatId, chatId));
46
-
47
  // Then insert the new messages
48
  return await db.insert(messages).values(dbMessages);
49
  }
50
  return null;
51
  } catch (error) {
52
- console.error('Failed to save messages in database', error);
53
  throw error;
54
  }
55
  }
56
 
57
  // Function to convert AI messages to DB format
58
- export function convertToDBMessages(aiMessages: AIMessage[], chatId: string): DBMessage[] {
59
- return aiMessages.map(msg => {
 
 
 
60
  // Use existing id or generate a new one
61
  const messageId = msg.id || nanoid();
62
-
63
  // If msg has parts, use them directly
64
  if (msg.parts) {
65
  return {
@@ -67,40 +76,77 @@ export function convertToDBMessages(aiMessages: AIMessage[], chatId: string): DB
67
  chatId,
68
  role: msg.role,
69
  parts: msg.parts,
70
- createdAt: new Date()
71
  };
72
  }
73
-
74
  // Otherwise, convert content to parts
75
  let parts: MessagePart[];
76
-
77
- if (typeof msg.content === 'string') {
78
- parts = [{ type: 'text', text: msg.content }];
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79
  } else if (Array.isArray(msg.content)) {
80
- if (msg.content.every(item => typeof item === 'object' && item !== null)) {
 
 
81
  // Content is already in parts-like format
82
  parts = msg.content as MessagePart[];
83
  } else {
84
  // Content is an array but not in parts format
85
- parts = [{ type: 'text', text: JSON.stringify(msg.content) }];
86
  }
87
  } else {
88
  // Default case
89
- parts = [{ type: 'text', text: String(msg.content) }];
90
  }
91
-
92
  return {
93
  id: messageId,
94
  chatId,
95
  role: msg.role,
96
  parts,
97
- createdAt: new Date()
98
  };
99
  });
100
  }
101
 
102
  // Convert DB messages to UI format
103
- export function convertToUIMessages(dbMessages: Array<Message>): Array<UIMessage> {
 
 
104
  return dbMessages.map((message) => ({
105
  id: message.id,
106
  parts: message.parts as MessagePart[],
@@ -110,108 +156,115 @@ export function convertToUIMessages(dbMessages: Array<Message>): Array<UIMessage
110
  }));
111
  }
112
 
113
- export async function saveChat({ id, userId, messages: aiMessages, title }: SaveChatParams) {
 
 
 
 
 
114
  // Generate a new ID if one wasn't provided
115
  const chatId = id || nanoid();
116
-
117
  // Check if title is provided, if not generate one
118
  let chatTitle = title;
119
-
120
  // Generate title if messages are provided and no title is specified
121
  if (aiMessages && aiMessages.length > 0) {
122
- const hasEnoughMessages = aiMessages.length >= 2 &&
123
- aiMessages.some(m => m.role === 'user') &&
124
- aiMessages.some(m => m.role === 'assistant');
125
-
126
- if (!chatTitle || chatTitle === 'New Chat' || chatTitle === undefined) {
 
127
  if (hasEnoughMessages) {
128
  try {
129
  // Use AI to generate a meaningful title based on conversation
130
  chatTitle = await generateTitle(aiMessages);
131
  } catch (error) {
132
- console.error('Error generating title:', error);
133
  // Fallback to basic title extraction if AI title generation fails
134
- const firstUserMessage = aiMessages.find(m => m.role === 'user');
135
  if (firstUserMessage) {
136
  // Check for parts first (new format)
137
- if (firstUserMessage.parts && Array.isArray(firstUserMessage.parts)) {
138
- const textParts = firstUserMessage.parts.filter((p: MessagePart) => p.type === 'text' && p.text);
 
 
 
 
 
139
  if (textParts.length > 0) {
140
- chatTitle = textParts[0].text?.slice(0, 50) || 'New Chat';
141
  if ((textParts[0].text?.length || 0) > 50) {
142
- chatTitle += '...';
143
  }
144
  } else {
145
- chatTitle = 'New Chat';
146
  }
147
- }
148
  // Fallback to content (old format)
149
- else if (typeof firstUserMessage.content === 'string') {
150
  chatTitle = firstUserMessage.content.slice(0, 50);
151
  if (firstUserMessage.content.length > 50) {
152
- chatTitle += '...';
153
  }
154
  } else {
155
- chatTitle = 'New Chat';
156
  }
157
  } else {
158
- chatTitle = 'New Chat';
159
  }
160
  }
161
  } else {
162
  // Not enough messages for AI title, use first message
163
- const firstUserMessage = aiMessages.find(m => m.role === 'user');
164
  if (firstUserMessage) {
165
  // Check for parts first (new format)
166
  if (firstUserMessage.parts && Array.isArray(firstUserMessage.parts)) {
167
- const textParts = firstUserMessage.parts.filter((p: MessagePart) => p.type === 'text' && p.text);
 
 
168
  if (textParts.length > 0) {
169
- chatTitle = textParts[0].text?.slice(0, 50) || 'New Chat';
170
  if ((textParts[0].text?.length || 0) > 50) {
171
- chatTitle += '...';
172
  }
173
  } else {
174
- chatTitle = 'New Chat';
175
  }
176
  }
177
  // Fallback to content (old format)
178
- else if (typeof firstUserMessage.content === 'string') {
179
  chatTitle = firstUserMessage.content.slice(0, 50);
180
  if (firstUserMessage.content.length > 50) {
181
- chatTitle += '...';
182
  }
183
  } else {
184
- chatTitle = 'New Chat';
185
  }
186
  } else {
187
- chatTitle = 'New Chat';
188
  }
189
  }
190
  }
191
  } else {
192
- chatTitle = chatTitle || 'New Chat';
193
  }
194
 
195
  // Check if chat already exists
196
  const existingChat = await db.query.chats.findFirst({
197
- where: and(
198
- eq(chats.id, chatId),
199
- eq(chats.userId, userId)
200
- ),
201
  });
202
 
203
  if (existingChat) {
204
  // Update existing chat
205
  await db
206
  .update(chats)
207
- .set({
208
  title: chatTitle,
209
- updatedAt: new Date()
210
  })
211
- .where(and(
212
- eq(chats.id, chatId),
213
- eq(chats.userId, userId)
214
- ));
215
  } else {
216
  // Create new chat
217
  await db.insert(chats).values({
@@ -219,7 +272,7 @@ export async function saveChat({ id, userId, messages: aiMessages, title }: Save
219
  userId,
220
  title: chatTitle,
221
  createdAt: new Date(),
222
- updatedAt: new Date()
223
  });
224
  }
225
 
@@ -231,48 +284,43 @@ export function getTextContent(message: Message): string {
231
  try {
232
  const parts = message.parts as MessagePart[];
233
  return parts
234
- .filter(part => part.type === 'text' && part.text)
235
- .map(part => part.text)
236
- .join('\n');
237
  } catch (e) {
238
  // If parsing fails, return empty string
239
- return '';
240
  }
241
  }
242
 
243
  export async function getChats(userId: string) {
244
  return await db.query.chats.findMany({
245
  where: eq(chats.userId, userId),
246
- orderBy: [desc(chats.updatedAt)]
247
  });
248
  }
249
 
250
- export async function getChatById(id: string, userId: string): Promise<ChatWithMessages | null> {
 
 
 
251
  const chat = await db.query.chats.findFirst({
252
- where: and(
253
- eq(chats.id, id),
254
- eq(chats.userId, userId)
255
- ),
256
  });
257
 
258
  if (!chat) return null;
259
 
260
  const chatMessages = await db.query.messages.findMany({
261
  where: eq(messages.chatId, id),
262
- orderBy: [messages.createdAt]
263
  });
264
 
265
  return {
266
  ...chat,
267
- messages: chatMessages
268
  };
269
  }
270
 
271
  export async function deleteChat(id: string, userId: string) {
272
- await db.delete(chats).where(
273
- and(
274
- eq(chats.id, id),
275
- eq(chats.userId, userId)
276
- )
277
- );
278
- }
 
1
  import { db } from "./db";
2
+ import {
3
+ chats,
4
+ messages,
5
+ type Chat,
6
+ type Message,
7
+ MessageRole,
8
+ type MessagePart,
9
+ type DBMessage,
10
+ } from "./db/schema";
11
  import { eq, desc, and } from "drizzle-orm";
12
  import { nanoid } from "nanoid";
13
  import { generateTitle } from "@/app/actions";
 
46
  try {
47
  if (dbMessages.length > 0) {
48
  const chatId = dbMessages[0].chatId;
49
+
50
  // First delete any existing messages for this chat
51
+ await db.delete(messages).where(eq(messages.chatId, chatId));
52
+
 
 
53
  // Then insert the new messages
54
  return await db.insert(messages).values(dbMessages);
55
  }
56
  return null;
57
  } catch (error) {
58
+ console.error("Failed to save messages in database", error);
59
  throw error;
60
  }
61
  }
62
 
63
  // Function to convert AI messages to DB format
64
+ export function convertToDBMessages(
65
+ aiMessages: AIMessage[],
66
+ chatId: string
67
+ ): DBMessage[] {
68
+ return aiMessages.map((msg) => {
69
  // Use existing id or generate a new one
70
  const messageId = msg.id || nanoid();
71
+
72
  // If msg has parts, use them directly
73
  if (msg.parts) {
74
  return {
 
76
  chatId,
77
  role: msg.role,
78
  parts: msg.parts,
79
+ createdAt: new Date(),
80
  };
81
  }
82
+
83
  // Otherwise, convert content to parts
84
  let parts: MessagePart[];
85
+
86
+ if (typeof msg.content === "string") {
87
+ const thinkRegex = /<think>([\s\S]*?)<\/think>/g;
88
+ const content = msg.content;
89
+ parts = [];
90
+ let lastIndex = 0;
91
+ let match;
92
+
93
+ while ((match = thinkRegex.exec(content)) !== null) {
94
+ // Add text part before the think tag
95
+ if (match.index > lastIndex) {
96
+ parts.push({
97
+ type: "text",
98
+ text: content.substring(lastIndex, match.index),
99
+ });
100
+ }
101
+
102
+ // Add the thinking part
103
+ parts.push({
104
+ type: "thinking",
105
+ thinking: match[1],
106
+ details: [{ type: "text", text: match[1] }],
107
+ });
108
+
109
+ lastIndex = thinkRegex.lastIndex;
110
+ }
111
+
112
+ // Add any remaining text part
113
+ if (lastIndex < content.length) {
114
+ parts.push({ type: "text", text: content.substring(lastIndex) });
115
+ }
116
+
117
+ // If no thinking tags were found, treat the whole content as a single text part
118
+ if (parts.length === 0) {
119
+ parts.push({ type: "text", text: content });
120
+ }
121
  } else if (Array.isArray(msg.content)) {
122
+ if (
123
+ msg.content.every((item) => typeof item === "object" && item !== null)
124
+ ) {
125
  // Content is already in parts-like format
126
  parts = msg.content as MessagePart[];
127
  } else {
128
  // Content is an array but not in parts format
129
+ parts = [{ type: "text", text: JSON.stringify(msg.content) }];
130
  }
131
  } else {
132
  // Default case
133
+ parts = [{ type: "text", text: String(msg.content) }];
134
  }
135
+
136
  return {
137
  id: messageId,
138
  chatId,
139
  role: msg.role,
140
  parts,
141
+ createdAt: new Date(),
142
  };
143
  });
144
  }
145
 
146
  // Convert DB messages to UI format
147
+ export function convertToUIMessages(
148
+ dbMessages: Array<Message>
149
+ ): Array<UIMessage> {
150
  return dbMessages.map((message) => ({
151
  id: message.id,
152
  parts: message.parts as MessagePart[],
 
156
  }));
157
  }
158
 
159
+ export async function saveChat({
160
+ id,
161
+ userId,
162
+ messages: aiMessages,
163
+ title,
164
+ }: SaveChatParams) {
165
  // Generate a new ID if one wasn't provided
166
  const chatId = id || nanoid();
167
+
168
  // Check if title is provided, if not generate one
169
  let chatTitle = title;
170
+
171
  // Generate title if messages are provided and no title is specified
172
  if (aiMessages && aiMessages.length > 0) {
173
+ const hasEnoughMessages =
174
+ aiMessages.length >= 2 &&
175
+ aiMessages.some((m) => m.role === "user") &&
176
+ aiMessages.some((m) => m.role === "assistant");
177
+
178
+ if (!chatTitle || chatTitle === "New Chat" || chatTitle === undefined) {
179
  if (hasEnoughMessages) {
180
  try {
181
  // Use AI to generate a meaningful title based on conversation
182
  chatTitle = await generateTitle(aiMessages);
183
  } catch (error) {
184
+ console.error("Error generating title:", error);
185
  // Fallback to basic title extraction if AI title generation fails
186
+ const firstUserMessage = aiMessages.find((m) => m.role === "user");
187
  if (firstUserMessage) {
188
  // Check for parts first (new format)
189
+ if (
190
+ firstUserMessage.parts &&
191
+ Array.isArray(firstUserMessage.parts)
192
+ ) {
193
+ const textParts = firstUserMessage.parts.filter(
194
+ (p: MessagePart) => p.type === "text" && p.text
195
+ );
196
  if (textParts.length > 0) {
197
+ chatTitle = textParts[0].text?.slice(0, 50) || "New Chat";
198
  if ((textParts[0].text?.length || 0) > 50) {
199
+ chatTitle += "...";
200
  }
201
  } else {
202
+ chatTitle = "New Chat";
203
  }
204
+ }
205
  // Fallback to content (old format)
206
+ else if (typeof firstUserMessage.content === "string") {
207
  chatTitle = firstUserMessage.content.slice(0, 50);
208
  if (firstUserMessage.content.length > 50) {
209
+ chatTitle += "...";
210
  }
211
  } else {
212
+ chatTitle = "New Chat";
213
  }
214
  } else {
215
+ chatTitle = "New Chat";
216
  }
217
  }
218
  } else {
219
  // Not enough messages for AI title, use first message
220
+ const firstUserMessage = aiMessages.find((m) => m.role === "user");
221
  if (firstUserMessage) {
222
  // Check for parts first (new format)
223
  if (firstUserMessage.parts && Array.isArray(firstUserMessage.parts)) {
224
+ const textParts = firstUserMessage.parts.filter(
225
+ (p: MessagePart) => p.type === "text" && p.text
226
+ );
227
  if (textParts.length > 0) {
228
+ chatTitle = textParts[0].text?.slice(0, 50) || "New Chat";
229
  if ((textParts[0].text?.length || 0) > 50) {
230
+ chatTitle += "...";
231
  }
232
  } else {
233
+ chatTitle = "New Chat";
234
  }
235
  }
236
  // Fallback to content (old format)
237
+ else if (typeof firstUserMessage.content === "string") {
238
  chatTitle = firstUserMessage.content.slice(0, 50);
239
  if (firstUserMessage.content.length > 50) {
240
+ chatTitle += "...";
241
  }
242
  } else {
243
+ chatTitle = "New Chat";
244
  }
245
  } else {
246
+ chatTitle = "New Chat";
247
  }
248
  }
249
  }
250
  } else {
251
+ chatTitle = chatTitle || "New Chat";
252
  }
253
 
254
  // Check if chat already exists
255
  const existingChat = await db.query.chats.findFirst({
256
+ where: and(eq(chats.id, chatId), eq(chats.userId, userId)),
 
 
 
257
  });
258
 
259
  if (existingChat) {
260
  // Update existing chat
261
  await db
262
  .update(chats)
263
+ .set({
264
  title: chatTitle,
265
+ updatedAt: new Date(),
266
  })
267
+ .where(and(eq(chats.id, chatId), eq(chats.userId, userId)));
 
 
 
268
  } else {
269
  // Create new chat
270
  await db.insert(chats).values({
 
272
  userId,
273
  title: chatTitle,
274
  createdAt: new Date(),
275
+ updatedAt: new Date(),
276
  });
277
  }
278
 
 
284
  try {
285
  const parts = message.parts as MessagePart[];
286
  return parts
287
+ .filter((part) => part.type === "text" && part.text)
288
+ .map((part) => part.text)
289
+ .join("\n");
290
  } catch (e) {
291
  // If parsing fails, return empty string
292
+ return "";
293
  }
294
  }
295
 
296
  export async function getChats(userId: string) {
297
  return await db.query.chats.findMany({
298
  where: eq(chats.userId, userId),
299
+ orderBy: [desc(chats.updatedAt)],
300
  });
301
  }
302
 
303
+ export async function getChatById(
304
+ id: string,
305
+ userId: string
306
+ ): Promise<ChatWithMessages | null> {
307
  const chat = await db.query.chats.findFirst({
308
+ where: and(eq(chats.id, id), eq(chats.userId, userId)),
 
 
 
309
  });
310
 
311
  if (!chat) return null;
312
 
313
  const chatMessages = await db.query.messages.findMany({
314
  where: eq(messages.chatId, id),
315
+ orderBy: [messages.createdAt],
316
  });
317
 
318
  return {
319
  ...chat,
320
+ messages: chatMessages,
321
  };
322
  }
323
 
324
  export async function deleteChat(id: string, userId: string) {
325
+ await db.delete(chats).where(and(eq(chats.id, id), eq(chats.userId, userId)));
326
+ }
 
 
 
 
 
lib/openai-stream.ts CHANGED
@@ -1,8 +1,3 @@
1
- import {
2
- createParser,
3
- type ParsedEvent,
4
- type ReconnectInterval,
5
- } from "eventsource-parser";
6
  import type { ChatCompletionChunk } from "openai/resources";
7
 
8
  export function createOpenAIStream(
 
 
 
 
 
 
1
  import type { ChatCompletionChunk } from "openai/resources";
2
 
3
  export function createOpenAIStream(