feat: fixed search parameter functionality and form component
This commit is contained in:
parent
489e6b556b
commit
d7434ce63d
@ -16,9 +16,9 @@ import ReactMarkdown from 'react-markdown';
|
|||||||
import { useTheme } from 'next-themes';
|
import { useTheme } from 'next-themes';
|
||||||
import Marked, { ReactRenderer } from 'marked-react';
|
import Marked, { ReactRenderer } from 'marked-react';
|
||||||
import { track } from '@vercel/analytics';
|
import { track } from '@vercel/analytics';
|
||||||
import { useSearchParams } from 'next/navigation';
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
import { useChat } from 'ai/react';
|
import { useChat } from 'ai/react';
|
||||||
import { ToolInvocation } from 'ai';
|
import { Message, ToolInvocation } from 'ai';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { motion, AnimatePresence } from 'framer-motion';
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
@ -118,13 +118,20 @@ interface Attachment {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const HomeContent = () => {
|
const HomeContent = () => {
|
||||||
|
const router = useRouter();
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
const initialQuery = searchParams.get('query') || '';
|
const initialQuery = searchParams.get('query') || '';
|
||||||
const initialModel = searchParams.get('model') || 'azure:gpt4o-mini';
|
const initialModel = searchParams.get('model') || 'azure:gpt4o-mini';
|
||||||
|
|
||||||
const lastSubmittedQueryRef = useRef(initialQuery);
|
// Memoize initial values to prevent re-calculation
|
||||||
const [hasSubmitted, setHasSubmitted] = useState(!!initialQuery);
|
const initialState = useMemo(() => ({
|
||||||
const [selectedModel, setSelectedModel] = useState(initialModel);
|
query: searchParams.get('query') || '',
|
||||||
|
model: searchParams.get('model') || 'azure:gpt4o-mini'
|
||||||
|
}), []); // Empty dependency array as we only want this on mount
|
||||||
|
|
||||||
|
const lastSubmittedQueryRef = useRef(initialState.query);
|
||||||
|
const [hasSubmitted, setHasSubmitted] = useState(() => !!initialState.query);
|
||||||
|
const [selectedModel, setSelectedModel] = useState(initialState.model);
|
||||||
const bottomRef = useRef<HTMLDivElement>(null);
|
const bottomRef = useRef<HTMLDivElement>(null);
|
||||||
const [suggestedQuestions, setSuggestedQuestions] = useState<string[]>([]);
|
const [suggestedQuestions, setSuggestedQuestions] = useState<string[]>([]);
|
||||||
const [isEditingMessage, setIsEditingMessage] = useState(false);
|
const [isEditingMessage, setIsEditingMessage] = useState(false);
|
||||||
@ -132,6 +139,7 @@ const HomeContent = () => {
|
|||||||
const [attachments, setAttachments] = useState<Attachment[]>([]);
|
const [attachments, setAttachments] = useState<Attachment[]>([]);
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
const inputRef = useRef<HTMLTextAreaElement>(null);
|
const inputRef = useRef<HTMLTextAreaElement>(null);
|
||||||
|
const initializedRef = useRef(false);
|
||||||
|
|
||||||
const { theme } = useTheme();
|
const { theme } = useTheme();
|
||||||
|
|
||||||
@ -162,6 +170,19 @@ const HomeContent = () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!initializedRef.current && initialState.query && !messages.length) {
|
||||||
|
initializedRef.current = true;
|
||||||
|
setHasSubmitted(true);
|
||||||
|
console.log("[initial query]:", initialState.query);
|
||||||
|
append({
|
||||||
|
content: initialState.query,
|
||||||
|
role: 'user'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [initialState.query, append, setInput, messages.length]);
|
||||||
|
|
||||||
|
|
||||||
const ThemeToggle: React.FC = () => {
|
const ThemeToggle: React.FC = () => {
|
||||||
const { theme, setTheme } = useTheme();
|
const { theme, setTheme } = useTheme();
|
||||||
|
|
||||||
@ -219,10 +240,9 @@ const HomeContent = () => {
|
|||||||
id: "1",
|
id: "1",
|
||||||
title: "New Updates!",
|
title: "New Updates!",
|
||||||
images: [
|
images: [
|
||||||
"https://metwm7frkvew6tn1.public.blob.vercel-storage.com/mplx-changelogs/mplx-maps-beta.png",
|
"https://metwm7frkvew6tn1.public.blob.vercel-storage.com/mplx-changelogs/mplx-new-claude-models.png",
|
||||||
"https://metwm7frkvew6tn1.public.blob.vercel-storage.com/mplx-changelogs/mplx-multi-run.png",
|
"https://metwm7frkvew6tn1.public.blob.vercel-storage.com/mplx-changelogs/mplx-nearby-search-maps-demo.png",
|
||||||
"https://metwm7frkvew6tn1.public.blob.vercel-storage.com/mplx-changelogs/mplx-multi-results.png",
|
"https://metwm7frkvew6tn1.public.blob.vercel-storage.com/mplx-changelogs/mplx-multi-search-demo.png"
|
||||||
"https://metwm7frkvew6tn1.public.blob.vercel-storage.com/mplx-changelogs/mplx-new-claude.png"
|
|
||||||
],
|
],
|
||||||
content:
|
content:
|
||||||
`## **Nearby Map Search Beta**
|
`## **Nearby Map Search Beta**
|
||||||
@ -283,8 +303,12 @@ The new Anthropic models: Claude 3.5 Sonnet and 3.5 Haiku models are now availab
|
|||||||
<h3 className="text-2xl font-medium font-serif text-neutral-800 dark:text-neutral-100">{changelog.title}</h3>
|
<h3 className="text-2xl font-medium font-serif text-neutral-800 dark:text-neutral-100">{changelog.title}</h3>
|
||||||
<ReactMarkdown
|
<ReactMarkdown
|
||||||
components={{
|
components={{
|
||||||
h2: ({ node, className, ...props }) => <h2 {...props} className={cn(className, "my-1 text-neutral-800 dark:text-neutral-100")} />,
|
h2: ({ node, className, ...props }) => (
|
||||||
p: ({ node, className, ...props }) => <p {...props} className={cn(className, "mb-2 text-neutral-700 dark:text-neutral-300")} />,
|
<h2 {...props} className={cn("my-2 text-lg font-medium text-neutral-800 dark:text-neutral-100", className)} />
|
||||||
|
),
|
||||||
|
p: ({ node, className, ...props }) => (
|
||||||
|
<p {...props} className={cn("mb-3 text-neutral-700 dark:text-neutral-300 leading-relaxed", className)} />
|
||||||
|
),
|
||||||
}}
|
}}
|
||||||
className="text-sm"
|
className="text-sm"
|
||||||
>
|
>
|
||||||
|
|||||||
@ -24,10 +24,10 @@ interface ModelSwitcherProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const models = [
|
const models = [
|
||||||
{ value: "azure:gpt4o-mini", label: "GPT-4o Mini", icon: Zap, description: "God speed, good quality", color: "emerald" },
|
{ value: "azure:gpt4o-mini", label: "GPT-4o Mini", icon: Zap, description: "God speed, good quality", color: "emerald", vision: true },
|
||||||
{ value: "anthropic:claude-3-5-haiku-20241022", label: "Claude 3.5 Haiku", icon: Sparkles, description: "Good quality, high speed", color: "orange" },
|
{ value: "anthropic:claude-3-5-haiku-20241022", label: "Claude 3.5 Haiku", icon: Sparkles, description: "Good quality, high speed", color: "orange", vision: false },
|
||||||
{ value: "anthropic:claude-3-5-sonnet-latest", label: "Claude 3.5 Sonnet (New)", icon: Sparkles, description: "High quality, good speed", color: "indigo" },
|
{ value: "anthropic:claude-3-5-sonnet-latest", label: "Claude 3.5 Sonnet (New)", icon: Sparkles, description: "High quality, good speed", color: "indigo", vision: true },
|
||||||
{ value: "azure:gpt-4o", label: "GPT-4o", icon: Cpu, description: "Higher quality, normal speed", color: "blue" },
|
{ value: "azure:gpt-4o", label: "GPT-4o", icon: Cpu, description: "Higher quality, normal speed", color: "blue", vision: true },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
||||||
@ -71,13 +71,13 @@ const ModelSwitcher: React.FC<ModelSwitcherProps> = ({ selectedModel, setSelecte
|
|||||||
"flex items-center justify-center w-8 h-8 rounded-full transition-all duration-300",
|
"flex items-center justify-center w-8 h-8 rounded-full transition-all duration-300",
|
||||||
getColorClasses(selectedModelData.color, true),
|
getColorClasses(selectedModelData.color, true),
|
||||||
"focus:outline-none focus:ring-2 focus:ring-opacity-50",
|
"focus:outline-none focus:ring-2 focus:ring-opacity-50",
|
||||||
`focus:ring-${selectedModelData.color}-500`,
|
`!focus:ring-${selectedModelData.color}-500`,
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<selectedModelData.icon className="w-4 h-4" />
|
<selectedModelData.icon className="w-4 h-4" />
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent className="w-[220px] p-1 !font-sans rounded-md shadow-md bg-white dark:bg-neutral-800 ml-4 sm:m-auto">
|
<DropdownMenuContent className="w-[220px] p-1 !font-sans rounded-md shadow-md bg-white dark:bg-neutral-800 ml-4 !mt-0 sm:m-auto !z-[52]">
|
||||||
{models.map((model) => (
|
{models.map((model) => (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
key={model.value}
|
key={model.value}
|
||||||
@ -170,6 +170,11 @@ const PaperclipIcon = ({ size = 16 }: { size?: number }) => {
|
|||||||
|
|
||||||
const MAX_IMAGES = 3;
|
const MAX_IMAGES = 3;
|
||||||
|
|
||||||
|
const hasVisionSupport = (modelValue: string): boolean => {
|
||||||
|
const selectedModel = models.find(model => model.value === modelValue);
|
||||||
|
return selectedModel?.vision === true
|
||||||
|
};
|
||||||
|
|
||||||
interface FormComponentProps {
|
interface FormComponentProps {
|
||||||
input: string;
|
input: string;
|
||||||
setInput: (input: string) => void;
|
setInput: (input: string) => void;
|
||||||
@ -312,23 +317,18 @@ const FormComponent: React.FC<FormComponentProps> = ({
|
|||||||
const [uploadQueue, setUploadQueue] = useState<Array<string>>([]);
|
const [uploadQueue, setUploadQueue] = useState<Array<string>>([]);
|
||||||
const { width } = useWindowSize();
|
const { width } = useWindowSize();
|
||||||
const postSubmitFileInputRef = useRef<HTMLInputElement>(null);
|
const postSubmitFileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const [isFocused, setIsFocused] = useState(false);
|
||||||
const adjustHeight = useCallback(() => {
|
|
||||||
if (inputRef.current) {
|
|
||||||
inputRef.current.style.height = "auto";
|
|
||||||
inputRef.current.style.height = `${inputRef.current.scrollHeight + 2}px`;
|
|
||||||
}
|
|
||||||
}, [inputRef]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (inputRef.current) {
|
|
||||||
adjustHeight();
|
|
||||||
}
|
|
||||||
}, [adjustHeight, input, inputRef]);
|
|
||||||
|
|
||||||
const handleInput = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
|
const handleInput = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
setInput(event.target.value);
|
setInput(event.target.value);
|
||||||
adjustHeight();
|
};
|
||||||
|
|
||||||
|
const handleFocus = () => {
|
||||||
|
setIsFocused(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBlur = () => {
|
||||||
|
setIsFocused(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const uploadFile = async (file: File): Promise<Attachment> => {
|
const uploadFile = async (file: File): Promise<Attachment> => {
|
||||||
@ -392,7 +392,7 @@ const FormComponent: React.FC<FormComponentProps> = ({
|
|||||||
if (input.trim() || attachments.length > 0) {
|
if (input.trim() || attachments.length > 0) {
|
||||||
setHasSubmitted(true);
|
setHasSubmitted(true);
|
||||||
lastSubmittedQueryRef.current = input.trim();
|
lastSubmittedQueryRef.current = input.trim();
|
||||||
track("search input", {query: input.trim()})
|
track("search input", { query: input.trim() })
|
||||||
|
|
||||||
handleSubmit(event, {
|
handleSubmit(event, {
|
||||||
experimental_attachments: attachments,
|
experimental_attachments: attachments,
|
||||||
@ -432,7 +432,8 @@ const FormComponent: React.FC<FormComponentProps> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn(
|
<div className={cn(
|
||||||
"relative w-full flex flex-col gap-2 rounded-lg transition-all duration-300 z-[99]",
|
"relative w-full flex flex-col gap-2 rounded-lg transition-all duration-300 z-[51]",
|
||||||
|
|
||||||
attachments.length > 0 || uploadQueue.length > 0
|
attachments.length > 0 || uploadQueue.length > 0
|
||||||
? "bg-gray-100/70 dark:bg-neutral-800 p-1"
|
? "bg-gray-100/70 dark:bg-neutral-800 p-1"
|
||||||
: "bg-transparent"
|
: "bg-transparent"
|
||||||
@ -473,14 +474,16 @@ const FormComponent: React.FC<FormComponentProps> = ({
|
|||||||
value={input}
|
value={input}
|
||||||
onChange={handleInput}
|
onChange={handleInput}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
|
onFocus={handleFocus}
|
||||||
|
onBlur={handleBlur}
|
||||||
className={cn(
|
className={cn(
|
||||||
"min-h-[48px] overflow-hidden resize-none rounded-lg text-base",
|
"min-h-[40px] max-h-[200px] w-full resize-none rounded-lg",
|
||||||
|
"overflow-y-auto overflow-x-hidden",
|
||||||
|
"text-base leading-relaxed",
|
||||||
"bg-neutral-100 dark:bg-neutral-900",
|
"bg-neutral-100 dark:bg-neutral-900",
|
||||||
"text-neutral-900 dark:text-neutral-100",
|
"text-neutral-900 dark:text-neutral-100",
|
||||||
"border border-neutral-200 dark:border-neutral-700",
|
|
||||||
"focus:border-neutral-300 dark:focus:border-neutral-600",
|
|
||||||
"focus:ring-2 focus:ring-neutral-300 dark:focus:ring-neutral-600",
|
"focus:ring-2 focus:ring-neutral-300 dark:focus:ring-neutral-600",
|
||||||
"pr-20 py-2"
|
"px-4 pt-3 pb-10" // Increased bottom padding to prevent overlap
|
||||||
)}
|
)}
|
||||||
rows={3}
|
rows={3}
|
||||||
onKeyDown={(event) => {
|
onKeyDown={(event) => {
|
||||||
@ -495,49 +498,53 @@ const FormComponent: React.FC<FormComponentProps> = ({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="absolute left-2 bottom-2 mt-4">
|
<div className={cn("absolute bottom-0 inset-x-0 flex justify-between items-center rounded-b-lg p-2 bg-neutral-100 dark:bg-neutral-900",
|
||||||
|
"border border-t-0",
|
||||||
|
)}>
|
||||||
<ModelSwitcher
|
<ModelSwitcher
|
||||||
selectedModel={selectedModel}
|
selectedModel={selectedModel}
|
||||||
setSelectedModel={setSelectedModel}
|
setSelectedModel={setSelectedModel}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="absolute right-2 bottom-2 flex items-center gap-2 mt-4">
|
<div className="flex items-center gap-2">
|
||||||
<Button
|
{hasVisionSupport(selectedModel) && (
|
||||||
className="rounded-full p-1.5 h-8 w-8 bg-white dark:bg-neutral-700 text-neutral-700 dark:text-neutral-300 hover:bg-neutral-300 dark:hover:bg-neutral-600"
|
<Button
|
||||||
onClick={(event) => {
|
className="rounded-full p-1.5 h-8 w-8 bg-white dark:bg-neutral-700 text-neutral-700 dark:text-neutral-300 hover:bg-neutral-300 dark:hover:bg-neutral-600"
|
||||||
event.preventDefault();
|
onClick={(event) => {
|
||||||
triggerFileInput();
|
event.preventDefault();
|
||||||
}}
|
triggerFileInput();
|
||||||
variant="outline"
|
}}
|
||||||
disabled={isLoading}
|
variant="outline"
|
||||||
>
|
disabled={isLoading}
|
||||||
<PaperclipIcon size={14} />
|
>
|
||||||
</Button>
|
<PaperclipIcon size={14} />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<Button
|
<Button
|
||||||
className="rounded-full p-1.5 h-8 w-8"
|
className="rounded-full p-1.5 h-8 w-8"
|
||||||
onClick={(event) => {
|
onClick={(event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
stop();
|
stop();
|
||||||
}}
|
}}
|
||||||
variant="destructive"
|
variant="destructive"
|
||||||
>
|
>
|
||||||
<StopIcon size={14} />
|
<StopIcon size={14} />
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<Button
|
<Button
|
||||||
className="rounded-full p-1.5 h-8 w-8 "
|
className="rounded-full p-1.5 h-8 w-8"
|
||||||
onClick={(event) => {
|
onClick={(event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
submitForm();
|
submitForm();
|
||||||
}}
|
}}
|
||||||
disabled={input.length === 0 && attachments.length === 0 || uploadQueue.length > 0}
|
disabled={input.length === 0 && attachments.length === 0 || uploadQueue.length > 0}
|
||||||
>
|
>
|
||||||
<ArrowUpIcon size={14} />
|
<ArrowUpIcon size={14} />
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user