Skip to content

Commit e4184b2

Browse files
committed
Add search-related components to the application
Introduced several new components for search functionality including SearchResults, SearchSection, SearchRelated, and VideoSearchSection. These components enhance the user experience by displaying search results, related queries, and search videos in an organized manner. Additional components for theme toggling and history viewing were also integrated to improve navigation and usability. Took 30 seconds
1 parent a4ab74d commit e4184b2

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+2875
-0
lines changed

app/search/[id]/page.tsx

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { notFound, redirect } from 'next/navigation'
2+
import { Chat } from '@/app/search/components/chat'
3+
import { getChat } from '@/lib/actions/chat'
4+
import { AI } from '@/app/search/actions'
5+
6+
export const maxDuration = 60
7+
8+
export interface SearchPageProps {
9+
params: {
10+
id: string
11+
}
12+
}
13+
14+
export async function generateMetadata({ params }: SearchPageProps) {
15+
const chat = await getChat(params.id, 'anonymous')
16+
return {
17+
title: chat?.title.toString().slice(0, 50) || 'Search'
18+
}
19+
}
20+
21+
export default async function SearchPage({ params }: SearchPageProps) {
22+
const userId = 'anonymous'
23+
const chat = await getChat(params.id, userId)
24+
25+
if (!chat) {
26+
redirect('/')
27+
}
28+
29+
if (chat?.userId !== userId) {
30+
notFound()
31+
}
32+
33+
return (
34+
<AI
35+
initialAIState={{
36+
chatId: chat.id,
37+
messages: chat.messages
38+
}}
39+
>
40+
<Chat id={params.id} />
41+
</AI>
42+
)
43+
}

app/search/actions.tsx

Lines changed: 330 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,330 @@
1+
import {
2+
StreamableValue,
3+
createAI,
4+
createStreamableUI,
5+
createStreamableValue,
6+
getAIState,
7+
getMutableAIState
8+
} from 'ai/rsc'
9+
import { CoreMessage, generateId } from 'ai'
10+
import { Section } from '@/app/search/components/section'
11+
import { FollowupPanel } from '@/app/search/components/followup-panel'
12+
import { saveChat } from '@/lib/actions/chat'
13+
import { Chat } from '@/lib/types/index'
14+
import { AIMessage } from '@/lib/types/index'
15+
import { UserMessage } from '@/app/search/components/user-message'
16+
import { SearchSection } from '@/app/search/components/search-section'
17+
import SearchRelated from '@/app/search/components/search-related'
18+
import { CopilotDisplay } from '@/app/search/components/copilot-display'
19+
import RetrieveSection from '@/app/search/components/retrieve-section'
20+
import { VideoSearchSection } from '@/app/search/components/video-search-section'
21+
import { AnswerSection } from '@/app/search/components/answer-section'
22+
import { workflow } from '@/lib/actions/workflow'
23+
24+
const MAX_MESSAGES = 6
25+
26+
async function submit(
27+
formData?: FormData,
28+
skip?: boolean,
29+
retryMessages?: AIMessage[]
30+
) {
31+
'use server'
32+
console.log('🚀 Submit action started:', { skip, hasFormData: !!formData })
33+
34+
const aiState = getMutableAIState<typeof AI>()
35+
const uiStream = createStreamableUI()
36+
const isGenerating = createStreamableValue(true)
37+
const isCollapsed = createStreamableValue(false)
38+
39+
const aiMessages = [...(retryMessages ?? aiState.get().messages)]
40+
// Get the messages from the state, filter out the tool messages
41+
const messages: CoreMessage[] = aiMessages
42+
.filter(
43+
message =>
44+
message.role !== 'tool' &&
45+
message.type !== 'followup' &&
46+
message.type !== 'related' &&
47+
message.type !== 'end'
48+
)
49+
.map(message => {
50+
const { role, content } = message
51+
return { role, content } as CoreMessage
52+
})
53+
54+
// Limit the number of messages to the maximum
55+
messages.splice(0, Math.max(messages.length - MAX_MESSAGES, 0))
56+
// Get the user input from the form data
57+
const userInput = skip
58+
? `{"action": "skip"}`
59+
: (formData?.get('input') as string)
60+
61+
const content = skip
62+
? userInput
63+
: formData
64+
? JSON.stringify(Object.fromEntries(formData))
65+
: null
66+
const type = skip
67+
? undefined
68+
: formData?.has('input')
69+
? 'input'
70+
: formData?.has('related_query')
71+
? 'input_related'
72+
: 'inquiry'
73+
74+
// Add the user message to the state
75+
if (content) {
76+
console.log('📝 Adding user message:', { type, contentPreview: content.substring(0, 100) })
77+
aiState.update({
78+
...aiState.get(),
79+
messages: [
80+
...aiState.get().messages,
81+
{
82+
id: generateId(),
83+
role: 'user',
84+
content,
85+
type
86+
}
87+
]
88+
})
89+
messages.push({
90+
role: 'user',
91+
content
92+
})
93+
}
94+
95+
// Run the agent workflow
96+
workflow(
97+
{ uiStream, isCollapsed, isGenerating },
98+
aiState,
99+
messages,
100+
skip ?? false
101+
)
102+
103+
console.log('✅ Submit action completed')
104+
return {
105+
id: generateId(),
106+
isGenerating: isGenerating.value,
107+
component: uiStream.value,
108+
isCollapsed: isCollapsed.value
109+
}
110+
}
111+
112+
export type AIState = {
113+
messages: AIMessage[]
114+
chatId: string
115+
isSharePage?: boolean
116+
}
117+
118+
export type UIState = {
119+
id: string
120+
component: React.ReactNode
121+
isGenerating?: StreamableValue<boolean>
122+
isCollapsed?: StreamableValue<boolean>
123+
}[]
124+
125+
const initialAIState: AIState = {
126+
chatId: generateId(),
127+
messages: []
128+
}
129+
130+
const initialUIState: UIState = []
131+
132+
// AI is a provider you wrap your application with so you can access AI and UI state in your components.
133+
export const AI = createAI<AIState, UIState>({
134+
actions: {
135+
submit
136+
},
137+
initialUIState,
138+
initialAIState,
139+
onGetUIState: async () => {
140+
'use server'
141+
console.log('🔍 Getting UI state')
142+
143+
const aiState = getAIState()
144+
if (aiState) {
145+
const uiState = getUIStateFromAIState(aiState as Chat)
146+
return uiState
147+
} else {
148+
return
149+
}
150+
},
151+
onSetAIState: async ({ state, done }) => {
152+
'use server'
153+
console.log('💾 Starting onSetAIState:', {
154+
chatId: state.chatId,
155+
messageCount: state.messages.length,
156+
done
157+
})
158+
159+
// Check if there is any message of type 'answer' in the state messages
160+
if (!state.messages.some(e => e.type === 'answer')) {
161+
console.log('⏭️ Skipping save - no answer message found')
162+
return
163+
}
164+
165+
const { chatId, messages } = state
166+
const path = `/search/${chatId}`
167+
console.log('🛣️ Generated path:', path)
168+
169+
const createdAt = new Date()
170+
const userId = 'anonymous'
171+
const title =
172+
messages.length > 0
173+
? JSON.parse(messages[0].content)?.input?.substring(0, 100) ||
174+
'Untitled'
175+
: 'Untitled'
176+
// Add an 'end' message at the end to determine if the history needs to be reloaded
177+
const updatedMessages: AIMessage[] = [
178+
...messages,
179+
{
180+
id: generateId(),
181+
role: 'assistant',
182+
content: `end`,
183+
type: 'end'
184+
}
185+
]
186+
187+
const chat: Chat = {
188+
id: chatId,
189+
createdAt,
190+
userId,
191+
path,
192+
title,
193+
messages: updatedMessages
194+
}
195+
196+
try {
197+
await saveChat(chat)
198+
console.log('✨ Chat saved successfully:', {
199+
chatId: chat.id,
200+
path,
201+
done,
202+
messageCount: chat.messages.length
203+
})
204+
} catch (error) {
205+
console.error('❌ Error saving chat:', error)
206+
}
207+
}
208+
})
209+
210+
export const getUIStateFromAIState = (aiState: Chat) => {
211+
console.log('🎨 Converting AI state to UI state:', {
212+
chatId: aiState.chatId,
213+
messageCount: aiState.messages.length
214+
})
215+
216+
const chatId = aiState.chatId
217+
const isSharePage = aiState.isSharePage
218+
219+
// Ensure messages is an array of plain objects
220+
const messages = Array.isArray(aiState.messages)
221+
? aiState.messages.map(msg => ({ ...msg }))
222+
: []
223+
224+
return messages
225+
.map((message, index) => {
226+
const { role, content, id, type, name } = message
227+
228+
if (
229+
!type ||
230+
type === 'end' ||
231+
(isSharePage && type === 'related') ||
232+
(isSharePage && type === 'followup')
233+
)
234+
return null
235+
236+
switch (role) {
237+
case 'user':
238+
switch (type) {
239+
case 'input':
240+
case 'input_related':
241+
const json = JSON.parse(content)
242+
const value = type === 'input' ? json.input : json.related_query
243+
return {
244+
id,
245+
component: (
246+
<UserMessage
247+
message={value}
248+
chatId={chatId}
249+
showShare={index === 0 && !isSharePage}
250+
/>
251+
)
252+
}
253+
case 'inquiry':
254+
return {
255+
id,
256+
component: <CopilotDisplay content={content} />
257+
}
258+
}
259+
case 'assistant':
260+
const answer = createStreamableValue()
261+
answer.done(content)
262+
switch (type) {
263+
case 'answer':
264+
return {
265+
id,
266+
component: <AnswerSection result={answer.value} />
267+
}
268+
case 'related':
269+
const relatedQueries = createStreamableValue()
270+
relatedQueries.done(JSON.parse(content))
271+
return {
272+
id,
273+
component: (
274+
<SearchRelated relatedQueries={relatedQueries.value} />
275+
)
276+
}
277+
case 'followup':
278+
return {
279+
id,
280+
component: (
281+
<Section title="Follow-up" className="pb-8">
282+
<FollowupPanel />
283+
</Section>
284+
)
285+
}
286+
}
287+
case 'tool':
288+
try {
289+
const toolOutput = JSON.parse(content)
290+
const isCollapsed = createStreamableValue()
291+
isCollapsed.done(true)
292+
const searchResults = createStreamableValue()
293+
searchResults.done(JSON.stringify(toolOutput))
294+
switch (name) {
295+
case 'search':
296+
return {
297+
id,
298+
component: <SearchSection result={searchResults.value} />,
299+
isCollapsed: isCollapsed.value
300+
}
301+
case 'retrieve':
302+
return {
303+
id,
304+
component: <RetrieveSection data={toolOutput} />,
305+
isCollapsed: isCollapsed.value
306+
}
307+
case 'videoSearch':
308+
return {
309+
id,
310+
component: (
311+
<VideoSearchSection result={searchResults.value} />
312+
),
313+
isCollapsed: isCollapsed.value
314+
}
315+
}
316+
} catch (error) {
317+
return {
318+
id,
319+
component: null
320+
}
321+
}
322+
default:
323+
return {
324+
id,
325+
component: null
326+
}
327+
}
328+
})
329+
.filter(message => message !== null) as UIState
330+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
'use client'
2+
3+
import { Section } from './section'
4+
import { BotMessage } from './message'
5+
6+
export type AnswerSectionProps = {
7+
result: string
8+
}
9+
10+
export function AnswerSectionGenerated({ result }: AnswerSectionProps) {
11+
return (
12+
<div>
13+
<Section title="Answer">
14+
<BotMessage content={result} />
15+
</Section>
16+
</div>
17+
)
18+
}

0 commit comments

Comments
 (0)