- 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>
194 lines
8.1 KiB
TypeScript
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: "Create a table about..." or "Generate an image of..."
|
|
</p>
|
|
</div>
|
|
</motion.div>
|
|
</PopoverPanel>
|
|
)}
|
|
</AnimatePresence>
|
|
</>
|
|
)}
|
|
</Popover>
|
|
);
|
|
};
|
|
|
|
export default InputBarPlus;
|