This commit is contained in:
parent
617780ef4b
commit
a4d2067d70
|
|
@ -11,84 +11,115 @@ import { Section } from '@/components/mpv2/section'
|
||||||
import { FollowupPanel } from '@/components/mpv2/followup-panel'
|
import { FollowupPanel } from '@/components/mpv2/followup-panel'
|
||||||
import { inquire, researcher, taskManager, querySuggestor } from '@/lib/agents'
|
import { inquire, researcher, taskManager, querySuggestor } from '@/lib/agents'
|
||||||
|
|
||||||
const initialAIState: ExperimentalMessage[] = []
|
async function submit(formData?: FormData, skip?: boolean) {
|
||||||
|
'use server'
|
||||||
|
|
||||||
|
const aiState = getMutableAIState<typeof AI>()
|
||||||
|
const uiStream = createStreamableUI()
|
||||||
|
const isGenerating = createStreamableValue(true)
|
||||||
|
|
||||||
|
const messages: ExperimentalMessage[] = aiState.get() as any
|
||||||
|
// Get the user input from the form data
|
||||||
|
const userInput = skip
|
||||||
|
? `{"action": "skip"}`
|
||||||
|
: (formData?.get('input') as string)
|
||||||
|
const content = skip
|
||||||
|
? userInput
|
||||||
|
: formData
|
||||||
|
? JSON.stringify(Object.fromEntries(formData))
|
||||||
|
: null
|
||||||
|
// Add the user message to the state
|
||||||
|
if (content) {
|
||||||
|
const message = { role: 'user', content }
|
||||||
|
messages.push(message as ExperimentalMessage)
|
||||||
|
aiState.update([...(aiState.get() as any), message])
|
||||||
|
}
|
||||||
|
|
||||||
|
async function processEvents() {
|
||||||
|
uiStream.update(<Spinner />)
|
||||||
|
|
||||||
|
let action: any = { object: { next: 'proceed' } }
|
||||||
|
// If the user skips the task, we proceed to the search
|
||||||
|
if (!skip) action = await taskManager(messages)
|
||||||
|
|
||||||
|
if (action.object.next === 'inquire') {
|
||||||
|
// Generate inquiry
|
||||||
|
const inquiry = await inquire(uiStream, messages)
|
||||||
|
|
||||||
|
uiStream.done()
|
||||||
|
isGenerating.done()
|
||||||
|
aiState.done([
|
||||||
|
...aiState.get(),
|
||||||
|
{ role: 'assistant', content: `inquiry: ${inquiry?.question}` }
|
||||||
|
])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate the answer
|
||||||
|
let answer = ''
|
||||||
|
let errorOccurred = false
|
||||||
|
const streamText = createStreamableValue<string>()
|
||||||
|
while (answer.length === 0) {
|
||||||
|
// Search the web and generate the answer
|
||||||
|
const { fullResponse, hasError } = await researcher(
|
||||||
|
uiStream,
|
||||||
|
streamText,
|
||||||
|
messages
|
||||||
|
)
|
||||||
|
answer = fullResponse
|
||||||
|
errorOccurred = hasError
|
||||||
|
}
|
||||||
|
streamText.done()
|
||||||
|
|
||||||
|
if (!errorOccurred) {
|
||||||
|
// Generate related queries
|
||||||
|
await querySuggestor(uiStream, messages)
|
||||||
|
|
||||||
|
// Add follow-up panel
|
||||||
|
uiStream.append(
|
||||||
|
<Section title="Follow-up">
|
||||||
|
<FollowupPanel />
|
||||||
|
</Section>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
isGenerating.done(false)
|
||||||
|
uiStream.done()
|
||||||
|
aiState.done([...aiState.get(), { role: 'assistant', content: answer }])
|
||||||
|
}
|
||||||
|
|
||||||
|
processEvents()
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: Date.now(),
|
||||||
|
isGenerating: isGenerating.value,
|
||||||
|
component: uiStream.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Define the initial state of the AI. It can be any JSON object.
|
||||||
|
const initialAIState: {
|
||||||
|
role: 'user' | 'assistant' | 'system' | 'function' | 'tool'
|
||||||
|
content: string
|
||||||
|
id?: string
|
||||||
|
name?: string
|
||||||
|
}[] = []
|
||||||
|
|
||||||
|
// The initial UI state that the client will keep track of, which contains the message IDs and their UI nodes.
|
||||||
const initialUIState: {
|
const initialUIState: {
|
||||||
id: number
|
id: number
|
||||||
isGenerating: StreamableValue<boolean>
|
isGenerating: StreamableValue<boolean>
|
||||||
component: React.ReactNode
|
component: React.ReactNode
|
||||||
}[] = []
|
}[] = []
|
||||||
|
|
||||||
|
// AI is a provider you wrap your application with so you can access AI and UI state in your components.
|
||||||
export const AI = createAI({
|
export const AI = createAI({
|
||||||
actions: {
|
actions: {
|
||||||
submit: async function(formData?: FormData, skip?: boolean) {
|
submit
|
||||||
'use server'
|
|
||||||
const aiState = getMutableAIState<typeof AI>()
|
|
||||||
const uiStream = createStreamableUI()
|
|
||||||
const isGenerating = createStreamableValue(true)
|
|
||||||
const messages: ExperimentalMessage[] = aiState.get() as any
|
|
||||||
const userInput = skip
|
|
||||||
? `{"action": "skip"}`
|
|
||||||
: (formData?.get('input') as string)
|
|
||||||
const content = skip
|
|
||||||
? userInput
|
|
||||||
: formData
|
|
||||||
? JSON.stringify(Object.fromEntries(formData))
|
|
||||||
: null
|
|
||||||
|
|
||||||
if (content) {
|
|
||||||
const message = { role: 'user', content }
|
|
||||||
messages.push(message as ExperimentalMessage)
|
|
||||||
aiState.update([...(aiState.get() as any), message])
|
|
||||||
}
|
|
||||||
|
|
||||||
async function processEvents() {
|
|
||||||
uiStream.update(<Spinner />)
|
|
||||||
let action: any = { object: { next: 'proceed' } }
|
|
||||||
if (!skip) action = await taskManager(messages)
|
|
||||||
if (action.object.next === 'inquire') {
|
|
||||||
const inquiry = await inquire(uiStream, messages)
|
|
||||||
uiStream.done()
|
|
||||||
isGenerating.done()
|
|
||||||
aiState.done([
|
|
||||||
...aiState.get(),
|
|
||||||
{ role: 'assistant', content: `inquiry: ${inquiry?.question}` }
|
|
||||||
])
|
|
||||||
return
|
|
||||||
}
|
|
||||||
let answer = ''
|
|
||||||
let errorOccurred = false
|
|
||||||
const streamText = createStreamableValue<string>()
|
|
||||||
while (answer.length === 0) {
|
|
||||||
const { fullResponse, hasError } = await researcher(
|
|
||||||
uiStream,
|
|
||||||
streamText,
|
|
||||||
messages
|
|
||||||
)
|
|
||||||
answer = fullResponse
|
|
||||||
errorOccurred = hasError
|
|
||||||
}
|
|
||||||
streamText.done()
|
|
||||||
if (!errorOccurred) {
|
|
||||||
await querySuggestor(uiStream, messages)
|
|
||||||
uiStream.append(
|
|
||||||
<Section title="Follow-up">
|
|
||||||
<FollowupPanel />
|
|
||||||
</Section>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
isGenerating.done(false)
|
|
||||||
uiStream.done()
|
|
||||||
aiState.done([...aiState.get(), { role: 'assistant', content: answer }])
|
|
||||||
}
|
|
||||||
processEvents()
|
|
||||||
return {
|
|
||||||
id: Date.now(),
|
|
||||||
isGenerating: isGenerating.value,
|
|
||||||
component: uiStream.value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
// Each state can be any shape of object, but for chat applications
|
||||||
|
// it makes sense to have an array of messages. Or you may prefer something like { id: number, messages: Message[] }
|
||||||
initialUIState,
|
initialUIState,
|
||||||
initialAIState
|
initialAIState
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -1,14 +1,12 @@
|
||||||
import type { Metadata, Viewport } from 'next'
|
import type { Metadata, Viewport } from 'next'
|
||||||
import { Inter as FontSans } from 'next/font/google'
|
import { Inter as FontSans } from 'next/font/google'
|
||||||
|
import { AI } from './action'
|
||||||
import '../../globals.css'
|
import '../../globals.css'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
import { ThemeProvider } from '@/components/mpv2/theme-provider'
|
import { ThemeProvider } from '@/components/mpv2/theme-provider'
|
||||||
import Header from '@/components/mpv2/header'
|
import Header from '@/components/mpv2/header'
|
||||||
import Footer from '@/components/mpv2/footer'
|
import Footer from '@/components/mpv2/footer'
|
||||||
|
|
||||||
import { AIProvider } from 'ai/rsc' // ✅ 关键点
|
|
||||||
import { AI } from './action' // ✅ 注意这里的引号
|
|
||||||
|
|
||||||
const fontSans = FontSans({
|
const fontSans = FontSans({
|
||||||
subsets: ['latin'],
|
subsets: ['latin'],
|
||||||
variable: '--font-sans'
|
variable: '--font-sans'
|
||||||
|
|
@ -41,14 +39,39 @@ export const viewport: Viewport = {
|
||||||
maximumScale: 1
|
maximumScale: 1
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ 这里类型一定要这样!不能直接 children: React.ReactNode
|
export default function RootLayout({
|
||||||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
children
|
||||||
|
}: Readonly<{
|
||||||
|
children: React.ReactNode
|
||||||
|
}>) {
|
||||||
|
// return (
|
||||||
|
// <html lang="en" suppressHydrationWarning>
|
||||||
|
// <body className={cn('font-sans antialiased', fontSans.variable)}>
|
||||||
|
// <ThemeProvider
|
||||||
|
// attribute="class"
|
||||||
|
// defaultTheme="system"
|
||||||
|
// enableSystem
|
||||||
|
// disableTransitionOnChange
|
||||||
|
// >
|
||||||
|
// <Header />
|
||||||
|
// <AI>{children}</AI>
|
||||||
|
// <Footer />
|
||||||
|
// </ThemeProvider>
|
||||||
|
// </body>
|
||||||
|
// </html>
|
||||||
|
// )
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ThemeProvider attribute="class" defaultTheme="system" enableSystem disableTransitionOnChange>
|
|
||||||
|
<ThemeProvider
|
||||||
|
attribute="class"
|
||||||
|
defaultTheme="system"
|
||||||
|
enableSystem
|
||||||
|
disableTransitionOnChange
|
||||||
|
>
|
||||||
{/* <Header /> */}
|
{/* <Header /> */}
|
||||||
<AIProvider value={AI}>
|
<AI>{children}</AI>
|
||||||
{children}
|
|
||||||
</AIProvider>
|
|
||||||
{/* <Footer /> */}
|
{/* <Footer /> */}
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue