feat: default locale Russian, geo determines language for other countries
- localization-svc: defaultLocale ru, resolveLocale only by geo - web-svc: DEFAULT_LOCALE ru, layout lang=ru, embeddedTranslations fallback ru - countryToLocale: default ru when no country or unknown country Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,194 @@
|
||||
'use client';
|
||||
|
||||
/**
|
||||
* Input bar «+» — меню: режимы, источники, Learn, Create, Model Council
|
||||
* docs/architecture: 01-perplexity-analogue-design.md §2.2.A
|
||||
*/
|
||||
|
||||
import { Plus, Zap, Sliders, Star, Globe, GraduationCap, Network, BookOpen, Users } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Popover, PopoverButton, PopoverPanel } from '@headlessui/react';
|
||||
import { useChat } from '@/lib/hooks/useChat';
|
||||
import { AnimatePresence, motion } from 'motion/react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
const MODES = [
|
||||
{ key: 'speed', label: 'Quick', icon: Zap },
|
||||
{ key: 'balanced', label: 'Pro', icon: Sliders },
|
||||
{ key: 'quality', label: 'Deep', icon: Star },
|
||||
] as const;
|
||||
|
||||
const SOURCES = [
|
||||
{ key: 'web', label: 'Web', icon: Globe },
|
||||
{ key: 'academic', label: 'Academic', icon: GraduationCap },
|
||||
{ key: 'discussions', label: 'Social', icon: Network },
|
||||
] as const;
|
||||
|
||||
const InputBarPlus = () => {
|
||||
const { optimizationMode, setOptimizationMode, sources, setSources } = useChat();
|
||||
const [learningMode, setLearningModeState] = useState(false);
|
||||
const [modelCouncil, setModelCouncilState] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setLearningModeState(typeof window !== 'undefined' && localStorage.getItem('learningMode') === 'true');
|
||||
setModelCouncilState(typeof window !== 'undefined' && localStorage.getItem('modelCouncil') === 'true');
|
||||
const handler = () => {
|
||||
setLearningModeState(localStorage.getItem('learningMode') === 'true');
|
||||
setModelCouncilState(localStorage.getItem('modelCouncil') === 'true');
|
||||
};
|
||||
window.addEventListener('client-config-changed', handler);
|
||||
return () => window.removeEventListener('client-config-changed', handler);
|
||||
}, []);
|
||||
|
||||
const setLearningMode = useCallback((v: boolean) => {
|
||||
localStorage.setItem('learningMode', String(v));
|
||||
setLearningModeState(v);
|
||||
window.dispatchEvent(new Event('client-config-changed'));
|
||||
}, []);
|
||||
|
||||
const setModelCouncil = useCallback((v: boolean) => {
|
||||
localStorage.setItem('modelCouncil', String(v));
|
||||
setModelCouncilState(v);
|
||||
window.dispatchEvent(new Event('client-config-changed'));
|
||||
}, []);
|
||||
|
||||
const toggleSource = useCallback(
|
||||
(key: string) => {
|
||||
if (sources.includes(key)) {
|
||||
setSources(sources.filter((s) => s !== key));
|
||||
} else {
|
||||
setSources([...sources, key]);
|
||||
}
|
||||
},
|
||||
[sources, setSources]
|
||||
);
|
||||
|
||||
return (
|
||||
<Popover className="relative">
|
||||
{({ open }) => (
|
||||
<>
|
||||
<PopoverButton
|
||||
type="button"
|
||||
title="More options"
|
||||
className={cn(
|
||||
'p-2 rounded-xl transition duration-200 focus:outline-none',
|
||||
'text-black/50 dark:text-white/50 hover:text-black dark:hover:text-white',
|
||||
'hover:bg-light-secondary dark:hover:bg-dark-secondary active:scale-95',
|
||||
open && 'bg-light-secondary dark:bg-dark-secondary text-[#EA580C]'
|
||||
)}
|
||||
>
|
||||
<Plus size={18} strokeWidth={2.5} />
|
||||
</PopoverButton>
|
||||
<AnimatePresence>
|
||||
{open && (
|
||||
<PopoverPanel
|
||||
static
|
||||
className="absolute z-20 w-72 left-0 bottom-full mb-2"
|
||||
>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.95 }}
|
||||
transition={{ duration: 0.12 }}
|
||||
className="origin-bottom-left rounded-xl border border-light-200 dark:border-dark-200 bg-light-primary dark:bg-dark-primary shadow-xl overflow-hidden"
|
||||
>
|
||||
<div className="p-2 border-b border-light-200/50 dark:border-dark-200/50">
|
||||
<p className="text-xs font-medium text-black/60 dark:text-white/60 px-2 py-1">Mode</p>
|
||||
<div className="flex gap-1">
|
||||
{MODES.map((m) => {
|
||||
const Icon = m.icon;
|
||||
const isActive = optimizationMode === m.key;
|
||||
return (
|
||||
<button
|
||||
key={m.key}
|
||||
type="button"
|
||||
onClick={() => setOptimizationMode(m.key)}
|
||||
className={cn(
|
||||
'flex-1 flex items-center justify-center gap-1.5 py-2 px-3 rounded-lg text-sm transition',
|
||||
isActive
|
||||
? 'bg-[#EA580C]/20 text-[#EA580C]'
|
||||
: 'hover:bg-light-200/50 dark:hover:bg-dark-200/50'
|
||||
)}
|
||||
>
|
||||
<Icon size={14} />
|
||||
{m.label}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-2 border-b border-light-200/50 dark:border-dark-200/50">
|
||||
<p className="text-xs font-medium text-black/60 dark:text-white/60 px-2 py-1">Sources</p>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{SOURCES.map((s) => {
|
||||
const Icon = s.icon;
|
||||
const checked = sources.includes(s.key);
|
||||
return (
|
||||
<button
|
||||
key={s.key}
|
||||
type="button"
|
||||
onClick={() => toggleSource(s.key)}
|
||||
className={cn(
|
||||
'flex items-center gap-1.5 py-1.5 px-2.5 rounded-lg text-sm transition',
|
||||
checked
|
||||
? 'bg-[#EA580C]/20 text-[#EA580C]'
|
||||
: 'hover:bg-light-200/50 dark:hover:bg-dark-200/50'
|
||||
)}
|
||||
>
|
||||
<Icon size={14} />
|
||||
{s.label}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-2 border-b border-light-200/50 dark:border-dark-200/50">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setLearningMode(!learningMode)}
|
||||
className={cn(
|
||||
'w-full flex items-center gap-2 py-2 px-3 rounded-lg text-sm transition',
|
||||
learningMode ? 'bg-[#EA580C]/20 text-[#EA580C]' : 'hover:bg-light-200/50 dark:hover:bg-dark-200/50'
|
||||
)}
|
||||
>
|
||||
<BookOpen size={16} />
|
||||
Step-by-step Learning
|
||||
{learningMode && <span className="ml-auto text-xs">On</span>}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="p-2 border-b border-light-200/50 dark:border-dark-200/50">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setModelCouncil(!modelCouncil)}
|
||||
className={cn(
|
||||
'w-full flex items-center gap-2 py-2 px-3 rounded-lg text-sm transition',
|
||||
modelCouncil ? 'bg-[#EA580C]/20 text-[#EA580C]' : 'hover:bg-light-200/50 dark:hover:bg-dark-200/50'
|
||||
)}
|
||||
title="Model Council (Max): 3 models in parallel → synthesis"
|
||||
>
|
||||
<Users size={16} />
|
||||
Model Council
|
||||
{modelCouncil && <span className="ml-auto text-xs">On</span>}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="p-2">
|
||||
<p className="text-xs font-medium text-black/60 dark:text-white/60 px-2 py-1">Create</p>
|
||||
<p className="text-xs text-black/50 dark:text-white/50 px-2 pb-2">
|
||||
Ask in chat: "Create a table about..." or "Generate an image of..."
|
||||
</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
</PopoverPanel>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</>
|
||||
)}
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
export default InputBarPlus;
|
||||
Reference in New Issue
Block a user