feat: update AI Chat, add ChatBot component
This commit is contained in:
parent
0794c7d297
commit
95a6f3b9d5
@ -1044,17 +1044,18 @@
|
|||||||
},
|
},
|
||||||
"AIImagePage": {
|
"AIImagePage": {
|
||||||
"title": "AI Image",
|
"title": "AI Image",
|
||||||
"description": "MkSaaS lets you make AI SaaS in days, simply and effortlessly",
|
"description": "MkSaaS lets you make AI SaaS in days, simply and effortlessly"
|
||||||
"content": "Working in progress"
|
},
|
||||||
|
"AIChatPage": {
|
||||||
|
"title": "AI Chat",
|
||||||
|
"description": "MkSaaS lets you make AI SaaS in days, simply and effortlessly"
|
||||||
},
|
},
|
||||||
"AIVideoPage": {
|
"AIVideoPage": {
|
||||||
"title": "AI Video",
|
"title": "AI Video",
|
||||||
"description": "MkSaaS lets you make AI SaaS in days, simply and effortlessly",
|
"description": "MkSaaS lets you make AI SaaS in days, simply and effortlessly"
|
||||||
"content": "Working in progress"
|
|
||||||
},
|
},
|
||||||
"AIAudioPage": {
|
"AIAudioPage": {
|
||||||
"title": "AI Audio",
|
"title": "AI Audio",
|
||||||
"description": "MkSaaS lets you make AI SaaS in days, simply and effortlessly",
|
"description": "MkSaaS lets you make AI SaaS in days, simply and effortlessly"
|
||||||
"content": "Working in progress"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1044,17 +1044,18 @@
|
|||||||
},
|
},
|
||||||
"AIImagePage": {
|
"AIImagePage": {
|
||||||
"title": "AI 图片",
|
"title": "AI 图片",
|
||||||
"description": "MkSaaS 让您在几天内轻松构建您的 AI SaaS,简单且毫不费力",
|
"description": "MkSaaS 让您在几天内轻松构建您的 AI SaaS,简单且毫不费力"
|
||||||
"content": "正在开发中"
|
},
|
||||||
|
"AIChatPage": {
|
||||||
|
"title": "AI 聊天",
|
||||||
|
"description": "MkSaaS 让您在几天内轻松构建您的 AI SaaS,简单且毫不费力"
|
||||||
},
|
},
|
||||||
"AIVideoPage": {
|
"AIVideoPage": {
|
||||||
"title": "AI 视频",
|
"title": "AI 视频",
|
||||||
"description": "MkSaaS 让您在几天内轻松构建您的 AI SaaS,简单且毫不费力",
|
"description": "MkSaaS 让您在几天内轻松构建您的 AI SaaS,简单且毫不费力"
|
||||||
"content": "正在开发中"
|
|
||||||
},
|
},
|
||||||
"AIAudioPage": {
|
"AIAudioPage": {
|
||||||
"title": "AI 音频",
|
"title": "AI 音频",
|
||||||
"description": "MkSaaS 让您在几天内轻松构建您的 AI SaaS,简单且毫不费力",
|
"description": "MkSaaS 让您在几天内轻松构建您的 AI SaaS,简单且毫不费力"
|
||||||
"content": "正在开发中"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
181
src/ai/chat/components/ChatBot.tsx
Normal file
181
src/ai/chat/components/ChatBot.tsx
Normal file
@ -0,0 +1,181 @@
|
|||||||
|
'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',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function ChatBot() {
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
@ -1,183 +1,44 @@
|
|||||||
'use client';
|
import ChatBot from '@/ai/chat/components/ChatBot';
|
||||||
|
import { constructMetadata } from '@/lib/metadata';
|
||||||
|
import { getUrlWithLocale } from '@/lib/urls/urls';
|
||||||
|
import { ZapIcon } from 'lucide-react';
|
||||||
|
import type { Metadata } from 'next';
|
||||||
|
import type { Locale } from 'next-intl';
|
||||||
|
import { getTranslations } from 'next-intl/server';
|
||||||
|
|
||||||
import {
|
export async function generateMetadata({
|
||||||
Conversation,
|
params,
|
||||||
ConversationContent,
|
}: {
|
||||||
ConversationScrollButton,
|
params: Promise<{ locale: Locale }>;
|
||||||
} from '@/components/ai-elements/conversation';
|
}): Promise<Metadata | undefined> {
|
||||||
import { Loader } from '@/components/ai-elements/loader';
|
const { locale } = await params;
|
||||||
import { Message, MessageContent } from '@/components/ai-elements/message';
|
const t = await getTranslations({ locale, namespace: 'Metadata' });
|
||||||
import {
|
const pt = await getTranslations({ locale, namespace: 'AIChatPage' });
|
||||||
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 = [
|
return constructMetadata({
|
||||||
{
|
title: pt('title') + ' | ' + t('title'),
|
||||||
name: 'GPT 4o',
|
description: pt('description'),
|
||||||
value: 'openai/gpt-4o',
|
canonicalUrl: getUrlWithLocale('/ai/chat', locale),
|
||||||
},
|
});
|
||||||
{
|
}
|
||||||
name: 'Deepseek R1',
|
|
||||||
value: 'deepseek/deepseek-r1',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const ChatBotDemo = () => {
|
export default async function AIChatPage() {
|
||||||
const [input, setInput] = useState('');
|
const t = await getTranslations('AIChatPage');
|
||||||
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 (
|
return (
|
||||||
<div className="max-w-4xl mx-auto p-6 relative size-full h-screen rounded-lg bg-muted/50">
|
<div className="min-h-screen bg-muted/50 rounded-lg">
|
||||||
<div className="flex flex-col h-full">
|
<div className="container mx-auto px-4 py-8 md:py-16">
|
||||||
<Conversation className="h-full">
|
{/* Header Section */}
|
||||||
<ConversationContent>
|
<div className="text-center space-y-6 mb-12">
|
||||||
{messages.map((message) => (
|
<div className="inline-flex items-center gap-2 px-4 py-2 rounded-full bg-primary/10 text-primary text-sm font-medium">
|
||||||
<div key={message.id}>
|
<ZapIcon className="size-4" />
|
||||||
{message.role === 'assistant' && (
|
{t('title')}
|
||||||
<Sources>
|
</div>
|
||||||
{message.parts.map((part, i) => {
|
</div>
|
||||||
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">
|
{/* Chat Bot */}
|
||||||
<PromptInputTextarea
|
<ChatBot />
|
||||||
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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
export default ChatBotDemo;
|
|
||||||
|
@ -26,7 +26,7 @@ export default async function AITextPage() {
|
|||||||
const t = await getTranslations('AITextPage');
|
const t = await getTranslations('AITextPage');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-background rounded-lg">
|
<div className="min-h-screen bg-muted/50 rounded-lg">
|
||||||
<div className="container mx-auto px-4 py-8 md:py-16">
|
<div className="container mx-auto px-4 py-8 md:py-16">
|
||||||
{/* Header Section */}
|
{/* Header Section */}
|
||||||
<div className="text-center space-y-6 mb-12">
|
<div className="text-center space-y-6 mb-12">
|
||||||
|
Loading…
Reference in New Issue
Block a user