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

View file

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

View file

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

View file

@ -12,8 +12,9 @@ const EmptyChatMessageInput = ({
focusMode: string;
setFocusMode: (mode: string) => void;
}) => {
const [copilotEnabled, setCopilotEnabled] = useState(false);
const [copilotToggled, setCopilotToggled] = useState(false);
const [message, setMessage] = useState('');
const copilotEnabled = localStorage.getItem('copilotEnabled') === 'true';
return (
<form
@ -45,10 +46,12 @@ const EmptyChatMessageInput = ({
{/* <Attach /> */}
</div>
<div className="flex flex-row items-center space-x-4 -mx-2">
<CopilotToggle
copilotEnabled={copilotEnabled}
setCopilotEnabled={setCopilotEnabled}
/>
{copilotEnabled && (
<CopilotToggle
copilotToggled={copilotToggled}
setCopilotToggled={setCopilotToggled}
/>
)}
<button
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"

View file

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

View file

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

View file

@ -1,8 +1,16 @@
import {BadgePercent, ChevronDown, CopyPlus, 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';
import {
BadgePercent,
ChevronDown,
CopyPlus,
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 = () => {
return (
@ -46,9 +54,9 @@ const focusModes = [
description: 'Search and watch videos',
icon: (
<SiYoutube
className="h-5 w-auto mr-0.5"
onPointerEnter={undefined}
onPointerLeave={undefined}
className="h-5 w-auto mr-0.5"
onPointerEnter={undefined}
onPointerLeave={undefined}
/>
),
},
@ -58,9 +66,9 @@ const focusModes = [
description: 'Search for discussions and opinions',
icon: (
<SiReddit
className="h-5 w-auto mr-0.5"
onPointerEnter={undefined}
onPointerLeave={undefined}
className="h-5 w-auto mr-0.5"
onPointerEnter={undefined}
onPointerLeave={undefined}
/>
),
},
@ -133,23 +141,23 @@ export const Focus = ({
};
export const CopilotToggle = ({
copilotEnabled,
setCopilotEnabled,
copilotToggled,
setCopilotToggled,
}: {
copilotEnabled: boolean;
setCopilotEnabled: (enabled: boolean) => void;
copilotToggled: boolean;
setCopilotToggled: (enabled: boolean) => void;
}) => {
return (
<div className="group flex flex-row items-center space-x-1 active:scale-95 duration-200 transition cursor-pointer">
<Switch
checked={copilotEnabled}
onChange={setCopilotEnabled}
checked={copilotToggled}
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"
>
<span className="sr-only">Copilot</span>
<span
className={cn(
copilotEnabled
copilotToggled
? 'translate-x-6 bg-[#24A0ED]'
: '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',
@ -157,10 +165,10 @@ export const CopilotToggle = ({
/>
</Switch>
<p
onClick={() => setCopilotEnabled(!copilotEnabled)}
onClick={() => setCopilotToggled(!copilotToggled)}
className={cn(
'text-xs font-medium transition-colors duration-150 ease-in-out',
copilotEnabled
copilotToggled
? 'text-[#24A0ED]'
: 'text-white/50 group-hover:text-white',
)}

View file

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

View file

@ -1,6 +1,7 @@
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 chatModelProvider = localStorage.getItem('chatModelProvider');
@ -10,7 +11,7 @@ export const getSuggestions = async (chatHisory: Message[]) => {
'Content-Type': 'application/json',
},
body: JSON.stringify({
chat_history: chatHisory,
chat_history: chatHistory,
chat_model: chatModel,
chat_model_provider: chatModelProvider,
}),
@ -20,3 +21,13 @@ export const getSuggestions = async (chatHisory: Message[]) => {
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;
}