feat: add copilot setting to config

refactor: move getConfig to actions, extracted Settings
This commit is contained in:
Justin Luoma 2024-05-25 08:04:40 -04:00
parent d788ca8eba
commit 79d4d87f24
10 changed files with 140 additions and 113 deletions

View file

@ -1,54 +1,55 @@
import fs from 'fs'; import fs from 'fs';
import path from 'path'; import path from 'path';
import {parse, stringify} from "smol-toml"; import { parse, stringify } from 'smol-toml';
const configFileName = 'config.toml'; const configFileName = 'config.toml';
interface Config { interface Config {
GENERAL: { GENERAL: {
PORT: number; PORT: number;
SIMILARITY_MEASURE: string; SIMILARITY_MEASURE: string;
}; ENABLE_COPILOT: boolean;
API_KEYS: { };
OPENAI: string; API_KEYS: {
GROQ: string; OPENAI: string;
}; GROQ: string;
API_ENDPOINTS: { };
SEARXNG: string; API_ENDPOINTS: {
OLLAMA: string; SEARXNG: string;
}; OLLAMA: string;
MODELS: [ };
{ MODELS: [
"name": string; {
"api_key": string; name: string;
"base_url": string; api_key: string;
"provider": string; base_url: string;
} provider: string;
]; },
EMBEDDINGS: [ ];
{ EMBEDDINGS: [
"name": string; {
"model": string; name: string;
"api_key": string; model: string;
"base_url": string; api_key: string;
"provider": string; base_url: string;
} provider: string;
]; },
];
} }
type RecursivePartial<T> = { type RecursivePartial<T> = {
[P in keyof T]?: RecursivePartial<T[P]>; [P in keyof T]?: RecursivePartial<T[P]>;
}; };
const loadConfig = () => const loadConfig = () =>
parse( parse(
fs.readFileSync(path.join(__dirname, `../${configFileName}`), 'utf-8'), fs.readFileSync(path.join(__dirname, `../${configFileName}`), 'utf-8'),
) as any as Config; ) as any as Config;
export const getPort = () => loadConfig().GENERAL.PORT; export const getPort = () => loadConfig().GENERAL.PORT;
export const getSimilarityMeasure = () => export const getSimilarityMeasure = () =>
loadConfig().GENERAL.SIMILARITY_MEASURE; loadConfig().GENERAL.SIMILARITY_MEASURE;
export const getOpenaiApiKey = () => loadConfig().API_KEYS.OPENAI; export const getOpenaiApiKey = () => loadConfig().API_KEYS.OPENAI;
@ -62,18 +63,17 @@ export const getCustomModels = () => loadConfig().MODELS;
export const getCustomEmbeddingModels = () => loadConfig().EMBEDDINGS; export const getCustomEmbeddingModels = () => loadConfig().EMBEDDINGS;
export const getCopilotEnabled = () => loadConfig().GENERAL.ENABLE_COPILOT;
export const updateConfig = (config: RecursivePartial<Config>) => { export const updateConfig = (config: RecursivePartial<Config>) => {
const currentConfig = loadConfig(); const currentConfig = loadConfig();
const updatedConfig = { const updatedConfig = {
...currentConfig, ...currentConfig,
...config ...config,
}; };
const toml = stringify(updatedConfig); const toml = stringify(updatedConfig);
fs.writeFileSync( fs.writeFileSync(path.join(__dirname, `../${configFileName}`), toml);
path.join(__dirname, `../${configFileName}`),
toml,
);
}; };

View file

@ -4,6 +4,7 @@ import {
getAvailableEmbeddingModelProviders, getAvailableEmbeddingModelProviders,
} from '../lib/providers'; } from '../lib/providers';
import { import {
getCopilotEnabled,
getGroqApiKey, getGroqApiKey,
getOllamaApiEndpoint, getOllamaApiEndpoint,
getOpenaiApiKey, getOpenaiApiKey,
@ -38,6 +39,7 @@ router.get('/', async (_, res) => {
config['openaiApiKey'] = getOpenaiApiKey(); config['openaiApiKey'] = getOpenaiApiKey();
config['ollamaApiUrl'] = getOllamaApiEndpoint(); config['ollamaApiUrl'] = getOllamaApiEndpoint();
config['groqApiKey'] = getGroqApiKey(); config['groqApiKey'] = getGroqApiKey();
config['copilotEnabled'] = getCopilotEnabled();
res.status(200).json(config); res.status(200).json(config);
}); });

View file

@ -7,7 +7,7 @@ import Chat from './Chat';
import EmptyChat from './EmptyChat'; import EmptyChat from './EmptyChat';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { useSearchParams } from 'next/navigation'; import { useSearchParams } from 'next/navigation';
import { getSuggestions } from '@/lib/actions'; import { getConfig, getSuggestions } from '@/lib/actions';
export type Message = { export type Message = {
id: string; id: string;
@ -156,6 +156,15 @@ const ChatWindow = () => {
messagesRef.current = messages; messagesRef.current = messages;
}, [messages]); }, [messages]);
useEffect(() => {
const fetchConfig = async () => {
const config = await getConfig();
localStorage.setItem('copilotEnabled', config.copilotEnabled.toString());
};
fetchConfig();
});
const sendMessage = async (message: string) => { const sendMessage = async (message: string) => {
if (loading) return; if (loading) return;
setLoading(true); setLoading(true);

View file

@ -12,8 +12,9 @@ const EmptyChatMessageInput = ({
focusMode: string; focusMode: string;
setFocusMode: (mode: string) => void; setFocusMode: (mode: string) => void;
}) => { }) => {
const [copilotEnabled, setCopilotEnabled] = useState(false); const [copilotToggled, setCopilotToggled] = useState(false);
const [message, setMessage] = useState(''); const [message, setMessage] = useState('');
const copilotEnabled = localStorage.getItem('copilotEnabled') === 'true';
return ( return (
<form <form
@ -45,10 +46,12 @@ const EmptyChatMessageInput = ({
{/* <Attach /> */} {/* <Attach /> */}
</div> </div>
<div className="flex flex-row items-center space-x-4 -mx-2"> <div className="flex flex-row items-center space-x-4 -mx-2">
<CopilotToggle {copilotEnabled && (
copilotEnabled={copilotEnabled} <CopilotToggle
setCopilotEnabled={setCopilotEnabled} copilotToggled={copilotToggled}
/> setCopilotToggled={setCopilotToggled}
/>
)}
<button <button
disabled={message.trim().length === 0} disabled={message.trim().length === 0}
className="bg-[#24A0ED] text-white disabled:text-white/50 hover:bg-opacity-85 transition duration-100 disabled:bg-[#ececec21] rounded-full p-2" className="bg-[#24A0ED] text-white disabled:text-white/50 hover:bg-opacity-85 transition duration-100 disabled:bg-[#ececec21] rounded-full p-2"

View file

@ -7,11 +7,10 @@ import { cn } from '@/lib/utils';
import { import {
BookCopy, BookCopy,
Disc3, Disc3,
Share,
Volume2,
StopCircle,
Layers3, Layers3,
Plus, Plus,
StopCircle,
Volume2,
} from 'lucide-react'; } from 'lucide-react';
import Markdown from 'markdown-to-jsx'; import Markdown from 'markdown-to-jsx';
import Copy from './MessageActions/Copy'; import Copy from './MessageActions/Copy';
@ -157,6 +156,7 @@ const MessageBox = ({
onClick={() => { onClick={() => {
sendMessage(suggestion); sendMessage(suggestion);
}} }}
role="button"
className="cursor-pointer flex flex-row justify-between font-medium space-x-2 items-center" className="cursor-pointer flex flex-row justify-between font-medium space-x-2 items-center"
> >
<p className="transition duration-200 hover:text-[#24A0ED]"> <p className="transition duration-200 hover:text-[#24A0ED]">

View file

@ -11,7 +11,7 @@ const MessageInput = ({
sendMessage: (message: string) => void; sendMessage: (message: string) => void;
loading: boolean; loading: boolean;
}) => { }) => {
const [copilotEnabled, setCopilotEnabled] = useState(false); const [copilotToggled, setCopilotToggled] = useState(false);
const [message, setMessage] = useState(''); const [message, setMessage] = useState('');
const [textareaRows, setTextareaRows] = useState(1); const [textareaRows, setTextareaRows] = useState(1);
const [mode, setMode] = useState<'multi' | 'single'>('single'); const [mode, setMode] = useState<'multi' | 'single'>('single');
@ -57,8 +57,8 @@ const MessageInput = ({
{mode === 'single' && ( {mode === 'single' && (
<div className="flex flex-row items-center space-x-4"> <div className="flex flex-row items-center space-x-4">
<CopilotToggle <CopilotToggle
copilotEnabled={copilotEnabled} copilotToggled={copilotToggled}
setCopilotEnabled={setCopilotEnabled} setCopilotToggled={setCopilotToggled}
/> />
<button <button
disabled={message.trim().length === 0 || loading} disabled={message.trim().length === 0 || loading}
@ -73,8 +73,8 @@ const MessageInput = ({
<Attach /> <Attach />
<div className="flex flex-row items-center space-x-4"> <div className="flex flex-row items-center space-x-4">
<CopilotToggle <CopilotToggle
copilotEnabled={copilotEnabled} copilotToggled={copilotToggled}
setCopilotEnabled={setCopilotEnabled} setCopilotToggled={setCopilotToggled}
/> />
<button <button
disabled={message.trim().length === 0 || loading} disabled={message.trim().length === 0 || loading}

View file

@ -1,8 +1,16 @@
import {BadgePercent, ChevronDown, CopyPlus, Globe, Pencil, ScanEye, SwatchBook,} from 'lucide-react'; import {
import {cn} from '@/lib/utils'; BadgePercent,
import {Popover, Switch, Transition} from '@headlessui/react'; ChevronDown,
import {SiReddit, SiYoutube} from '@icons-pack/react-simple-icons'; CopyPlus,
import {Fragment} from 'react'; Globe,
Pencil,
ScanEye,
SwatchBook,
} from 'lucide-react';
import { cn } from '@/lib/utils';
import { Popover, Switch, Transition } from '@headlessui/react';
import { SiReddit, SiYoutube } from '@icons-pack/react-simple-icons';
import { Fragment } from 'react';
export const Attach = () => { export const Attach = () => {
return ( return (
@ -46,9 +54,9 @@ const focusModes = [
description: 'Search and watch videos', description: 'Search and watch videos',
icon: ( icon: (
<SiYoutube <SiYoutube
className="h-5 w-auto mr-0.5" className="h-5 w-auto mr-0.5"
onPointerEnter={undefined} onPointerEnter={undefined}
onPointerLeave={undefined} onPointerLeave={undefined}
/> />
), ),
}, },
@ -58,9 +66,9 @@ const focusModes = [
description: 'Search for discussions and opinions', description: 'Search for discussions and opinions',
icon: ( icon: (
<SiReddit <SiReddit
className="h-5 w-auto mr-0.5" className="h-5 w-auto mr-0.5"
onPointerEnter={undefined} onPointerEnter={undefined}
onPointerLeave={undefined} onPointerLeave={undefined}
/> />
), ),
}, },
@ -133,23 +141,23 @@ export const Focus = ({
}; };
export const CopilotToggle = ({ export const CopilotToggle = ({
copilotEnabled, copilotToggled,
setCopilotEnabled, setCopilotToggled,
}: { }: {
copilotEnabled: boolean; copilotToggled: boolean;
setCopilotEnabled: (enabled: boolean) => void; setCopilotToggled: (enabled: boolean) => void;
}) => { }) => {
return ( return (
<div className="group flex flex-row items-center space-x-1 active:scale-95 duration-200 transition cursor-pointer"> <div className="group flex flex-row items-center space-x-1 active:scale-95 duration-200 transition cursor-pointer">
<Switch <Switch
checked={copilotEnabled} checked={copilotToggled}
onChange={setCopilotEnabled} onChange={setCopilotToggled}
className="bg-[#111111] border border-[#1C1C1C] relative inline-flex h-5 w-10 sm:h-6 sm:w-11 items-center rounded-full" className="bg-[#111111] border border-[#1C1C1C] relative inline-flex h-5 w-10 sm:h-6 sm:w-11 items-center rounded-full"
> >
<span className="sr-only">Copilot</span> <span className="sr-only">Copilot</span>
<span <span
className={cn( className={cn(
copilotEnabled copilotToggled
? 'translate-x-6 bg-[#24A0ED]' ? 'translate-x-6 bg-[#24A0ED]'
: 'translate-x-1 bg-white/50', : 'translate-x-1 bg-white/50',
'inline-block h-3 w-3 sm:h-4 sm:w-4 transform rounded-full transition-all duration-200', 'inline-block h-3 w-3 sm:h-4 sm:w-4 transform rounded-full transition-all duration-200',
@ -157,10 +165,10 @@ export const CopilotToggle = ({
/> />
</Switch> </Switch>
<p <p
onClick={() => setCopilotEnabled(!copilotEnabled)} onClick={() => setCopilotToggled(!copilotToggled)}
className={cn( className={cn(
'text-xs font-medium transition-colors duration-150 ease-in-out', 'text-xs font-medium transition-colors duration-150 ease-in-out',
copilotEnabled copilotToggled
? 'text-[#24A0ED]' ? 'text-[#24A0ED]'
: 'text-white/50 group-hover:text-white', : 'text-white/50 group-hover:text-white',
)} )}

View file

@ -1,18 +1,8 @@
import { Dialog, Transition } from '@headlessui/react'; import { Dialog, Transition } from '@headlessui/react';
import { CloudUpload, RefreshCcw, RefreshCw } from 'lucide-react'; import { CloudUpload, RefreshCcw, RefreshCw } from 'lucide-react';
import React, { Fragment, useEffect, useState } from 'react'; import React, { Fragment, useEffect, useState } from 'react';
import { Settings } from '@/types/Settings';
interface SettingsType { import { getConfig } from '@/lib/actions';
chatModelProviders: {
[key: string]: string[];
};
embeddingModelProviders: {
[key: string]: string[];
};
openaiApiKey: string;
groqApiKey: string;
ollamaApiUrl: string;
}
const SettingsDialog = ({ const SettingsDialog = ({
isOpen, isOpen,
@ -21,7 +11,7 @@ const SettingsDialog = ({
isOpen: boolean; isOpen: boolean;
setIsOpen: (isOpen: boolean) => void; setIsOpen: (isOpen: boolean) => void;
}) => { }) => {
const [config, setConfig] = useState<SettingsType | null>(null); const [config, setConfig] = useState<Settings | null>(null);
const [selectedChatModelProvider, setSelectedChatModelProvider] = useState< const [selectedChatModelProvider, setSelectedChatModelProvider] = useState<
string | null string | null
>(null); >(null);
@ -42,13 +32,7 @@ const SettingsDialog = ({
if (isOpen) { if (isOpen) {
const fetchConfig = async () => { const fetchConfig = async () => {
setIsLoading(true); setIsLoading(true);
const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/config`, { const data = await getConfig();
headers: {
'Content-Type': 'application/json',
},
});
const data = (await res.json()) as SettingsType;
setConfig(data); setConfig(data);
const chatModelProvidersKeys = Object.keys( const chatModelProvidersKeys = Object.keys(
@ -66,30 +50,28 @@ const SettingsDialog = ({
: ''; : '';
const chatModelProvider = const chatModelProvider =
localStorage.getItem('chatModelProvider') || (localStorage.getItem('chatModelProvider') ??
defaultChatModelProvider || defaultChatModelProvider) ||
''; '';
const chatModel = const chatModel =
localStorage.getItem('chatModel') || (localStorage.getItem('chatModel') ??
(data.chatModelProviders && data.chatModelProviders?.[chatModelProvider]?.[0]) ||
data.chatModelProviders[chatModelProvider]?.[0]) ||
''; '';
const embeddingModelProvider = const embeddingModelProvider =
localStorage.getItem('embeddingModelProvider') || (localStorage.getItem('embeddingModelProvider') ??
defaultEmbeddingModelProvider || defaultEmbeddingModelProvider) ||
''; '';
const embeddingModel = const embeddingModel =
localStorage.getItem('embeddingModel') || (localStorage.getItem('embeddingModel') ??
(data.embeddingModelProviders && data.embeddingModelProviders?.[embeddingModelProvider]?.[0]) ||
data.embeddingModelProviders[embeddingModelProvider]?.[0]) ||
''; '';
setSelectedChatModelProvider(chatModelProvider); setSelectedChatModelProvider(chatModelProvider);
setSelectedChatModel(chatModel); setSelectedChatModel(chatModel);
setSelectedEmbeddingModelProvider(embeddingModelProvider); setSelectedEmbeddingModelProvider(embeddingModelProvider);
setSelectedEmbeddingModel(embeddingModel); setSelectedEmbeddingModel(embeddingModel);
setCustomOpenAIApiKey(localStorage.getItem('openAIApiKey') || ''); setCustomOpenAIApiKey(localStorage.getItem('openAIApiKey') ?? '');
setCustomOpenAIBaseURL(localStorage.getItem('openAIBaseURL') || ''); setCustomOpenAIBaseURL(localStorage.getItem('openAIBaseURL') ?? '');
setIsLoading(false); setIsLoading(false);
}; };

View file

@ -1,6 +1,7 @@
import { Message } from '@/components/ChatWindow'; import { Message } from '@/components/ChatWindow';
import { Settings } from '@/types/Settings';
export const getSuggestions = async (chatHisory: Message[]) => { export const getSuggestions = async (chatHistory: Message[]) => {
const chatModel = localStorage.getItem('chatModel'); const chatModel = localStorage.getItem('chatModel');
const chatModelProvider = localStorage.getItem('chatModelProvider'); const chatModelProvider = localStorage.getItem('chatModelProvider');
@ -10,7 +11,7 @@ export const getSuggestions = async (chatHisory: Message[]) => {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
body: JSON.stringify({ body: JSON.stringify({
chat_history: chatHisory, chat_history: chatHistory,
chat_model: chatModel, chat_model: chatModel,
chat_model_provider: chatModelProvider, chat_model_provider: chatModelProvider,
}), }),
@ -20,3 +21,13 @@ export const getSuggestions = async (chatHisory: Message[]) => {
return data.suggestions; return data.suggestions;
}; };
export async function getConfig() {
const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/config`, {
headers: {
'Content-Type': 'application/json',
},
});
return (await res.json()) as Settings;
}

12
ui/types/Settings.ts Normal file
View file

@ -0,0 +1,12 @@
export interface Settings {
chatModelProviders: {
[key: string]: string[];
};
embeddingModelProviders: {
[key: string]: string[];
};
openaiApiKey: string;
groqApiKey: string;
ollamaApiUrl: string;
copilotEnabled: boolean;
}