Files
gooseek/services/web-svc/src/components/MessageInputActions/InputBarPlus.tsx
home 328d968f3f Deploy: migrate k3s → Docker; search logic → master-agents-svc
- deploy/k3s удалён, deploy/docker добавлен (Caddyfile, docker-compose, searxng)
- chat-svc: agents/models/prompts удалены, использует llm-svc (LLMClient, EmbeddingClient)
- master-agents-svc: SearchOrchestrator, classifier, researcher, actions, widgets
- web-svc: ChatModelSelector, Optimization, Sources удалены; InputBarPlus; UnregisterSW
- geo-device-svc, localization-svc: Dockerfiles
- docs: 02-k3s-services-spec.md, RUNBOOK/TELEMETRY/WORKING удалены

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-23 22:14:00 +03:00

194 lines
8.1 KiB
TypeScript

'use client';
/**
* Input bar «+» — меню: режимы, источники, Learn, Create, Model Council
*/
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: &quot;Create a table about...&quot; or &quot;Generate an image of...&quot;
</p>
</div>
</motion.div>
</PopoverPanel>
)}
</AnimatePresence>
</>
)}
</Popover>
);
};
export default InputBarPlus;