feat: implement AI chat functionality with new API route and chat component
This commit is contained in:
parent
3a61c953a4
commit
64ba2711aa
@ -181,6 +181,7 @@ CRON_JOBS_PASSWORD=""
|
|||||||
# AI
|
# AI
|
||||||
# https://mksaas.com/docs/ai
|
# https://mksaas.com/docs/ai
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
|
AI_GATEWAY_API_KEY=""
|
||||||
FAL_API_KEY=""
|
FAL_API_KEY=""
|
||||||
FIREWORKS_API_KEY=""
|
FIREWORKS_API_KEY=""
|
||||||
OPENAI_API_KEY=""
|
OPENAI_API_KEY=""
|
||||||
|
183
src/app/[locale]/(marketing)/ai/chat/page.tsx
Normal file
183
src/app/[locale]/(marketing)/ai/chat/page.tsx
Normal file
@ -0,0 +1,183 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Conversation,
|
||||||
|
ConversationContent,
|
||||||
|
ConversationScrollButton,
|
||||||
|
} from '@/components/ai-elements/conversation';
|
||||||
|
import { Loader } from '@/components/ai-elements/loader';
|
||||||
|
import { Message, MessageContent } from '@/components/ai-elements/message';
|
||||||
|
import {
|
||||||
|
PromptInput,
|
||||||
|
PromptInputButton,
|
||||||
|
PromptInputModelSelect,
|
||||||
|
PromptInputModelSelectContent,
|
||||||
|
PromptInputModelSelectItem,
|
||||||
|
PromptInputModelSelectTrigger,
|
||||||
|
PromptInputModelSelectValue,
|
||||||
|
PromptInputSubmit,
|
||||||
|
PromptInputTextarea,
|
||||||
|
PromptInputToolbar,
|
||||||
|
PromptInputTools,
|
||||||
|
} from '@/components/ai-elements/prompt-input';
|
||||||
|
import {
|
||||||
|
Reasoning,
|
||||||
|
ReasoningContent,
|
||||||
|
ReasoningTrigger,
|
||||||
|
} from '@/components/ai-elements/reasoning';
|
||||||
|
import { Response } from '@/components/ai-elements/response';
|
||||||
|
import {
|
||||||
|
Source,
|
||||||
|
Sources,
|
||||||
|
SourcesContent,
|
||||||
|
SourcesTrigger,
|
||||||
|
} from '@/components/ai-elements/source';
|
||||||
|
import { useChat } from '@ai-sdk/react';
|
||||||
|
import { GlobeIcon } from 'lucide-react';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
const models = [
|
||||||
|
{
|
||||||
|
name: 'GPT 4o',
|
||||||
|
value: 'openai/gpt-4o',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Deepseek R1',
|
||||||
|
value: 'deepseek/deepseek-r1',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const ChatBotDemo = () => {
|
||||||
|
const [input, setInput] = useState('');
|
||||||
|
const [model, setModel] = useState<string>(models[0].value);
|
||||||
|
const [webSearch, setWebSearch] = useState(false);
|
||||||
|
const { messages, sendMessage, status } = useChat();
|
||||||
|
|
||||||
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (input.trim()) {
|
||||||
|
sendMessage(
|
||||||
|
{ text: input },
|
||||||
|
{
|
||||||
|
body: {
|
||||||
|
model: model,
|
||||||
|
webSearch: webSearch,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
setInput('');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="max-w-4xl mx-auto p-6 relative size-full h-screen rounded-lg bg-muted/50">
|
||||||
|
<div className="flex flex-col h-full">
|
||||||
|
<Conversation className="h-full">
|
||||||
|
<ConversationContent>
|
||||||
|
{messages.map((message) => (
|
||||||
|
<div key={message.id}>
|
||||||
|
{message.role === 'assistant' && (
|
||||||
|
<Sources>
|
||||||
|
{message.parts.map((part, i) => {
|
||||||
|
switch (part.type) {
|
||||||
|
case 'source-url':
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<SourcesTrigger
|
||||||
|
count={
|
||||||
|
message.parts.filter(
|
||||||
|
(part) => part.type === 'source-url'
|
||||||
|
).length
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<SourcesContent key={`${message.id}-${i}`}>
|
||||||
|
<Source
|
||||||
|
key={`${message.id}-${i}`}
|
||||||
|
href={part.url}
|
||||||
|
title={part.url}
|
||||||
|
/>
|
||||||
|
</SourcesContent>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
</Sources>
|
||||||
|
)}
|
||||||
|
<Message from={message.role} key={message.id}>
|
||||||
|
<MessageContent>
|
||||||
|
{message.parts.map((part, i) => {
|
||||||
|
switch (part.type) {
|
||||||
|
case 'text':
|
||||||
|
return (
|
||||||
|
<Response key={`${message.id}-${i}`}>
|
||||||
|
{part.text}
|
||||||
|
</Response>
|
||||||
|
);
|
||||||
|
case 'reasoning':
|
||||||
|
return (
|
||||||
|
<Reasoning
|
||||||
|
key={`${message.id}-${i}`}
|
||||||
|
className="w-full"
|
||||||
|
isStreaming={status === 'streaming'}
|
||||||
|
>
|
||||||
|
<ReasoningTrigger />
|
||||||
|
<ReasoningContent>{part.text}</ReasoningContent>
|
||||||
|
</Reasoning>
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
</MessageContent>
|
||||||
|
</Message>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{status === 'submitted' && <Loader />}
|
||||||
|
</ConversationContent>
|
||||||
|
<ConversationScrollButton />
|
||||||
|
</Conversation>
|
||||||
|
|
||||||
|
<PromptInput onSubmit={handleSubmit} className="mt-4">
|
||||||
|
<PromptInputTextarea
|
||||||
|
onChange={(e) => setInput(e.target.value)}
|
||||||
|
value={input}
|
||||||
|
/>
|
||||||
|
<PromptInputToolbar>
|
||||||
|
<PromptInputTools>
|
||||||
|
<PromptInputButton
|
||||||
|
variant={webSearch ? 'default' : 'ghost'}
|
||||||
|
onClick={() => setWebSearch(!webSearch)}
|
||||||
|
>
|
||||||
|
<GlobeIcon size={16} />
|
||||||
|
<span>Search</span>
|
||||||
|
</PromptInputButton>
|
||||||
|
<PromptInputModelSelect
|
||||||
|
onValueChange={(value) => {
|
||||||
|
setModel(value);
|
||||||
|
}}
|
||||||
|
value={model}
|
||||||
|
>
|
||||||
|
<PromptInputModelSelectTrigger>
|
||||||
|
<PromptInputModelSelectValue />
|
||||||
|
</PromptInputModelSelectTrigger>
|
||||||
|
<PromptInputModelSelectContent>
|
||||||
|
{models.map((model) => (
|
||||||
|
<PromptInputModelSelectItem
|
||||||
|
key={model.value}
|
||||||
|
value={model.value}
|
||||||
|
>
|
||||||
|
{model.name}
|
||||||
|
</PromptInputModelSelectItem>
|
||||||
|
))}
|
||||||
|
</PromptInputModelSelectContent>
|
||||||
|
</PromptInputModelSelect>
|
||||||
|
</PromptInputTools>
|
||||||
|
<PromptInputSubmit disabled={!input} status={status} />
|
||||||
|
</PromptInputToolbar>
|
||||||
|
</PromptInput>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ChatBotDemo;
|
26
src/app/api/chat/route.ts
Normal file
26
src/app/api/chat/route.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { type UIMessage, convertToModelMessages, streamText } from 'ai';
|
||||||
|
|
||||||
|
// Allow streaming responses up to 30 seconds
|
||||||
|
export const maxDuration = 30;
|
||||||
|
|
||||||
|
export async function POST(req: Request) {
|
||||||
|
const {
|
||||||
|
messages,
|
||||||
|
model,
|
||||||
|
webSearch,
|
||||||
|
}: { messages: UIMessage[]; model: string; webSearch: boolean } =
|
||||||
|
await req.json();
|
||||||
|
|
||||||
|
const result = streamText({
|
||||||
|
model: webSearch ? 'perplexity/sonar' : model,
|
||||||
|
messages: convertToModelMessages(messages),
|
||||||
|
system:
|
||||||
|
'You are a helpful assistant that can answer questions and help with tasks',
|
||||||
|
});
|
||||||
|
|
||||||
|
// send sources and reasoning back to the client
|
||||||
|
return result.toUIMessageStreamResponse({
|
||||||
|
sendSources: true,
|
||||||
|
sendReasoning: true,
|
||||||
|
});
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user