Feature (OpenMemory): Add support for LLM and Embedding Providers in OpenMemory (#2794)

This commit is contained in:
Saket Aryan
2025-05-25 13:31:23 +05:30
committed by GitHub
parent b339cab3c1
commit 5c6fbcaab0
20 changed files with 1586 additions and 123 deletions

View File

@@ -0,0 +1,165 @@
"use client";
import { useState, useEffect } from "react"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { SaveIcon, RotateCcw } from "lucide-react"
import { FormView } from "@/components/form-view"
import { JsonEditor } from "@/components/json-editor"
import { useConfig } from "@/hooks/useConfig"
import { useSelector } from "react-redux"
import { RootState } from "@/store/store"
import { useToast } from "@/components/ui/use-toast"
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog"
export default function SettingsPage() {
const { toast } = useToast()
const configState = useSelector((state: RootState) => state.config)
const [settings, setSettings] = useState({
openmemory: configState.openmemory || {
custom_instructions: null
},
mem0: configState.mem0
})
const [viewMode, setViewMode] = useState<"form" | "json">("form")
const { fetchConfig, saveConfig, resetConfig, isLoading, error } = useConfig()
useEffect(() => {
// Load config from API on component mount
const loadConfig = async () => {
try {
await fetchConfig()
} catch (error) {
toast({
title: "Error",
description: "Failed to load configuration",
variant: "destructive",
})
}
}
loadConfig()
}, [])
// Update local state when redux state changes
useEffect(() => {
setSettings(prev => ({
...prev,
openmemory: configState.openmemory || { custom_instructions: null },
mem0: configState.mem0
}))
}, [configState.openmemory, configState.mem0])
const handleSave = async () => {
try {
await saveConfig({
openmemory: settings.openmemory,
mem0: settings.mem0
})
toast({
title: "Settings saved",
description: "Your configuration has been updated successfully.",
})
} catch (error) {
toast({
title: "Error",
description: "Failed to save configuration",
variant: "destructive",
})
}
}
const handleReset = async () => {
try {
await resetConfig()
toast({
title: "Settings reset",
description: "Configuration has been reset to default values.",
})
await fetchConfig()
} catch (error) {
toast({
title: "Error",
description: "Failed to reset configuration",
variant: "destructive",
})
}
}
return (
<div className="text-white py-6">
<div className="container mx-auto py-10 max-w-4xl">
<div className="flex justify-between items-center mb-8">
<div className="animate-fade-slide-down">
<h1 className="text-3xl font-bold tracking-tight">Settings</h1>
<p className="text-muted-foreground mt-1">Manage your OpenMemory and Mem0 configuration</p>
</div>
<div className="flex space-x-2">
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="outline" className="border-zinc-800 text-zinc-200 hover:bg-zinc-700 hover:text-zinc-50 animate-fade-slide-down" disabled={isLoading}>
<RotateCcw className="mr-2 h-4 w-4" />
Reset Defaults
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Reset Configuration?</AlertDialogTitle>
<AlertDialogDescription>
This will reset all settings to the system defaults. Any custom configuration will be lost.
API keys will be set to use environment variables.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={handleReset} className="bg-red-600 hover:bg-red-700">
Reset
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
<Button onClick={handleSave} className="bg-primary hover:bg-primary/90 animate-fade-slide-down" disabled={isLoading}>
<SaveIcon className="mr-2 h-4 w-4" />
{isLoading ? "Saving..." : "Save Configuration"}
</Button>
</div>
</div>
<Tabs value={viewMode} onValueChange={(value) => setViewMode(value as "form" | "json")} className="w-full animate-fade-slide-down delay-1">
<TabsList className="grid w-full grid-cols-2 mb-8">
<TabsTrigger value="form">Form View</TabsTrigger>
<TabsTrigger value="json">JSON Editor</TabsTrigger>
</TabsList>
<TabsContent value="form">
<FormView settings={settings} onChange={setSettings} />
</TabsContent>
<TabsContent value="json">
<Card>
<CardHeader>
<CardTitle>JSON Configuration</CardTitle>
<CardDescription>Edit the entire configuration directly as JSON</CardDescription>
</CardHeader>
<CardContent>
<JsonEditor value={settings} onChange={setSettings} />
</CardContent>
</Card>
</TabsContent>
</Tabs>
</div>
</div>
)
}

View File

@@ -11,6 +11,8 @@ import { useMemoriesApi } from "@/hooks/useMemoriesApi";
import Image from "next/image";
import { useStats } from "@/hooks/useStats";
import { useAppsApi } from "@/hooks/useAppsApi";
import { Settings } from "lucide-react";
import { useConfig } from "@/hooks/useConfig";
export function Navbar() {
const pathname = usePathname();
@@ -18,6 +20,7 @@ export function Navbar() {
const memoriesApi = useMemoriesApi();
const appsApi = useAppsApi();
const statsApi = useStats();
const configApi = useConfig();
// Define route matchers with typed parameter extraction
const routeBasedFetchMapping: {
@@ -52,6 +55,10 @@ export function Navbar() {
match: /^\/$/,
getFetchers: () => [statsApi.fetchStats, memoriesApi.fetchMemories],
},
{
match: /^\/settings$/,
getFetchers: () => [configApi.fetchConfig],
},
];
const getFetchersForPath = (path: string) => {
@@ -127,6 +134,18 @@ export function Navbar() {
Apps
</Button>
</Link>
<Link href="/settings">
<Button
variant="outline"
size="sm"
className={`flex items-center gap-2 border-none ${
isActive("/settings") ? activeClass : inactiveClass
}`}
>
<Settings />
Settings
</Button>
</Link>
</div>
<div className="flex items-center gap-4">
<Button

View File

@@ -0,0 +1,348 @@
"use client"
import { useState } from "react"
import { Eye, EyeOff } from "lucide-react"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "./ui/card"
import { Input } from "./ui/input"
import { Label } from "./ui/label"
import { Slider } from "./ui/slider"
import { Switch } from "./ui/switch"
import { Button } from "./ui/button"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select"
import { Textarea } from "./ui/textarea"
interface FormViewProps {
settings: any
onChange: (settings: any) => void
}
export function FormView({ settings, onChange }: FormViewProps) {
const [showLlmAdvanced, setShowLlmAdvanced] = useState(false)
const [showLlmApiKey, setShowLlmApiKey] = useState(false)
const [showEmbedderApiKey, setShowEmbedderApiKey] = useState(false)
const handleOpenMemoryChange = (key: string, value: any) => {
onChange({
...settings,
openmemory: {
...settings.openmemory,
[key]: value,
},
})
}
const handleLlmProviderChange = (value: string) => {
onChange({
...settings,
mem0: {
...settings.mem0,
llm: {
...settings.mem0.llm,
provider: value,
},
},
})
}
const handleLlmConfigChange = (key: string, value: any) => {
onChange({
...settings,
mem0: {
...settings.mem0,
llm: {
...settings.mem0.llm,
config: {
...settings.mem0.llm.config,
[key]: value,
},
},
},
})
}
const handleEmbedderProviderChange = (value: string) => {
onChange({
...settings,
mem0: {
...settings.mem0,
embedder: {
...settings.mem0.embedder,
provider: value,
},
},
})
}
const handleEmbedderConfigChange = (key: string, value: any) => {
onChange({
...settings,
mem0: {
...settings.mem0,
embedder: {
...settings.mem0.embedder,
config: {
...settings.mem0.embedder.config,
[key]: value,
},
},
},
})
}
const needsLlmApiKey = settings.mem0?.llm?.provider?.toLowerCase() !== "ollama"
const needsEmbedderApiKey = settings.mem0?.embedder?.provider?.toLowerCase() !== "ollama"
const isLlmOllama = settings.mem0?.llm?.provider?.toLowerCase() === "ollama"
const isEmbedderOllama = settings.mem0?.embedder?.provider?.toLowerCase() === "ollama"
const LLM_PROVIDERS = [
"OpenAI",
"Anthropic",
"Azure OpenAI",
"Ollama",
"Together",
"Groq",
"Litellm",
"Mistral AI",
"Google AI",
"AWS Bedrock",
"Gemini",
"DeepSeek",
"xAI",
"LM Studio",
"LangChain",
]
const EMBEDDER_PROVIDERS = [
"OpenAI",
"Azure OpenAI",
"Ollama",
"Hugging Face",
"Vertexai",
"Gemini",
"Lmstudio",
"Together",
"LangChain",
"AWS Bedrock",
]
return (
<div className="space-y-8">
{/* OpenMemory Settings */}
<Card>
<CardHeader>
<CardTitle>OpenMemory Settings</CardTitle>
<CardDescription>Configure your OpenMemory instance settings</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
<div className="space-y-2">
<Label htmlFor="custom-instructions">Custom Instructions</Label>
<Textarea
id="custom-instructions"
placeholder="Enter custom instructions for memory management..."
value={settings.openmemory?.custom_instructions || ""}
onChange={(e) => handleOpenMemoryChange("custom_instructions", e.target.value)}
className="min-h-[100px]"
/>
<p className="text-xs text-muted-foreground mt-1">
Custom instructions that will be used to guide memory processing and fact extraction.
</p>
</div>
</CardContent>
</Card>
{/* LLM Settings */}
<Card>
<CardHeader>
<CardTitle>LLM Settings</CardTitle>
<CardDescription>Configure your Large Language Model provider and settings</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
<div className="space-y-2">
<Label htmlFor="llm-provider">LLM Provider</Label>
<Select
value={settings.mem0?.llm?.provider || ""}
onValueChange={handleLlmProviderChange}
>
<SelectTrigger id="llm-provider">
<SelectValue placeholder="Select a provider" />
</SelectTrigger>
<SelectContent>
{LLM_PROVIDERS.map((provider) => (
<SelectItem key={provider} value={provider.toLowerCase()}>
{provider}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="llm-model">Model</Label>
<Input
id="llm-model"
placeholder="Enter model name"
value={settings.mem0?.llm?.config?.model || ""}
onChange={(e) => handleLlmConfigChange("model", e.target.value)}
/>
</div>
{isLlmOllama && (
<div className="space-y-2">
<Label htmlFor="llm-ollama-url">Ollama Base URL</Label>
<Input
id="llm-ollama-url"
placeholder="http://host.docker.internal:11434"
value={settings.mem0?.llm?.config?.ollama_base_url || ""}
onChange={(e) => handleLlmConfigChange("ollama_base_url", e.target.value)}
/>
<p className="text-xs text-muted-foreground mt-1">
Leave empty to use default: http://host.docker.internal:11434
</p>
</div>
)}
{needsLlmApiKey && (
<div className="space-y-2">
<Label htmlFor="llm-api-key">API Key</Label>
<div className="relative">
<Input
id="llm-api-key"
type={showLlmApiKey ? "text" : "password"}
placeholder="env:API_KEY"
value={settings.mem0?.llm?.config?.api_key || ""}
onChange={(e) => handleLlmConfigChange("api_key", e.target.value)}
/>
<Button
variant="ghost"
size="icon"
type="button"
className="absolute right-2 top-1/2 transform -translate-y-1/2 h-7 w-7"
onClick={() => setShowLlmApiKey(!showLlmApiKey)}
>
{showLlmApiKey ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
</Button>
</div>
<p className="text-xs text-muted-foreground mt-1">
Use "env:API_KEY" to load from environment variable, or enter directly
</p>
</div>
)}
<div className="flex items-center space-x-2 pt-2">
<Switch id="llm-advanced-settings" checked={showLlmAdvanced} onCheckedChange={setShowLlmAdvanced} />
<Label htmlFor="llm-advanced-settings">Show advanced settings</Label>
</div>
{showLlmAdvanced && (
<div className="space-y-6 pt-2">
<div className="space-y-2">
<div className="flex justify-between">
<Label htmlFor="temperature">Temperature: {settings.mem0?.llm?.config?.temperature}</Label>
</div>
<Slider
id="temperature"
min={0}
max={1}
step={0.1}
value={[settings.mem0?.llm?.config?.temperature || 0.7]}
onValueChange={(value) => handleLlmConfigChange("temperature", value[0])}
/>
</div>
<div className="space-y-2">
<Label htmlFor="max-tokens">Max Tokens</Label>
<Input
id="max-tokens"
type="number"
placeholder="2000"
value={settings.mem0?.llm?.config?.max_tokens || ""}
onChange={(e) => handleLlmConfigChange("max_tokens", Number.parseInt(e.target.value) || "")}
/>
</div>
</div>
)}
</CardContent>
</Card>
{/* Embedder Settings */}
<Card>
<CardHeader>
<CardTitle>Embedder Settings</CardTitle>
<CardDescription>Configure your Embedding Model provider and settings</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
<div className="space-y-2">
<Label htmlFor="embedder-provider">Embedder Provider</Label>
<Select
value={settings.mem0?.embedder?.provider || ""}
onValueChange={handleEmbedderProviderChange}
>
<SelectTrigger id="embedder-provider">
<SelectValue placeholder="Select a provider" />
</SelectTrigger>
<SelectContent>
{EMBEDDER_PROVIDERS.map((provider) => (
<SelectItem key={provider} value={provider.toLowerCase()}>
{provider}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="embedder-model">Model</Label>
<Input
id="embedder-model"
placeholder="Enter model name"
value={settings.mem0?.embedder?.config?.model || ""}
onChange={(e) => handleEmbedderConfigChange("model", e.target.value)}
/>
</div>
{isEmbedderOllama && (
<div className="space-y-2">
<Label htmlFor="embedder-ollama-url">Ollama Base URL</Label>
<Input
id="embedder-ollama-url"
placeholder="http://host.docker.internal:11434"
value={settings.mem0?.embedder?.config?.ollama_base_url || ""}
onChange={(e) => handleEmbedderConfigChange("ollama_base_url", e.target.value)}
/>
<p className="text-xs text-muted-foreground mt-1">
Leave empty to use default: http://host.docker.internal:11434
</p>
</div>
)}
{needsEmbedderApiKey && (
<div className="space-y-2">
<Label htmlFor="embedder-api-key">API Key</Label>
<div className="relative">
<Input
id="embedder-api-key"
type={showEmbedderApiKey ? "text" : "password"}
placeholder="env:API_KEY"
value={settings.mem0?.embedder?.config?.api_key || ""}
onChange={(e) => handleEmbedderConfigChange("api_key", e.target.value)}
/>
<Button
variant="ghost"
size="icon"
type="button"
className="absolute right-2 top-1/2 transform -translate-y-1/2 h-7 w-7"
onClick={() => setShowEmbedderApiKey(!showEmbedderApiKey)}
>
{showEmbedderApiKey ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
</Button>
</div>
<p className="text-xs text-muted-foreground mt-1">
Use "env:API_KEY" to load from environment variable, or enter directly
</p>
</div>
)}
</CardContent>
</Card>
</div>
)
}

View File

@@ -0,0 +1,79 @@
"use client"
import type React from "react"
import { useState, useEffect } from "react"
import { AlertCircle, CheckCircle2 } from "lucide-react"
import { Alert, AlertDescription } from "./ui/alert"
import { Button } from "./ui/button"
import { Textarea } from "./ui/textarea"
interface JsonEditorProps {
value: any
onChange: (value: any) => void
}
export function JsonEditor({ value, onChange }: JsonEditorProps) {
const [jsonString, setJsonString] = useState("")
const [error, setError] = useState<string | null>(null)
const [isValid, setIsValid] = useState(true)
useEffect(() => {
try {
setJsonString(JSON.stringify(value, null, 2))
setIsValid(true)
setError(null)
} catch (err) {
setError("Invalid JSON object")
setIsValid(false)
}
}, [value])
const handleTextChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
setJsonString(e.target.value)
try {
JSON.parse(e.target.value)
setIsValid(true)
setError(null)
} catch (err) {
setError("Invalid JSON syntax")
setIsValid(false)
}
}
const handleApply = () => {
try {
const parsed = JSON.parse(jsonString)
onChange(parsed)
setIsValid(true)
setError(null)
} catch (err) {
setError("Failed to apply changes: Invalid JSON")
}
}
return (
<div className="space-y-4">
<div className="relative">
<Textarea value={jsonString} onChange={handleTextChange} className="font-mono h-[600px] resize-none" />
<div className="absolute top-3 right-3">
{isValid ? (
<CheckCircle2 className="h-5 w-5 text-green-500" />
) : (
<AlertCircle className="h-5 w-5 text-red-500" />
)}
</div>
</div>
{error && (
<Alert variant="destructive">
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
<Button onClick={handleApply} disabled={!isValid} className="w-full">
Apply Changes
</Button>
</div>
)
}

View File

@@ -0,0 +1,131 @@
import { useState } from 'react';
import axios from 'axios';
import { useDispatch, useSelector } from 'react-redux';
import { AppDispatch, RootState } from '@/store/store';
import {
setConfigLoading,
setConfigSuccess,
setConfigError,
updateLLM,
updateEmbedder,
updateMem0Config,
updateOpenMemory,
LLMProvider,
EmbedderProvider,
Mem0Config,
OpenMemoryConfig
} from '@/store/configSlice';
interface UseConfigApiReturn {
fetchConfig: () => Promise<void>;
saveConfig: (config: { openmemory?: OpenMemoryConfig; mem0: Mem0Config }) => Promise<void>;
saveLLMConfig: (llmConfig: LLMProvider) => Promise<void>;
saveEmbedderConfig: (embedderConfig: EmbedderProvider) => Promise<void>;
resetConfig: () => Promise<void>;
isLoading: boolean;
error: string | null;
}
export const useConfig = (): UseConfigApiReturn => {
const [isLoading, setIsLoading] = useState<boolean>(false);
const [error, setError] = useState<string | null>(null);
const dispatch = useDispatch<AppDispatch>();
const URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8765";
const fetchConfig = async () => {
setIsLoading(true);
dispatch(setConfigLoading());
try {
const response = await axios.get(`${URL}/api/v1/config`);
dispatch(setConfigSuccess(response.data));
setIsLoading(false);
} catch (err: any) {
const errorMessage = err.response?.data?.detail || err.message || 'Failed to fetch configuration';
dispatch(setConfigError(errorMessage));
setError(errorMessage);
setIsLoading(false);
throw new Error(errorMessage);
}
};
const saveConfig = async (config: { openmemory?: OpenMemoryConfig; mem0: Mem0Config }) => {
setIsLoading(true);
setError(null);
try {
const response = await axios.put(`${URL}/api/v1/config`, config);
dispatch(setConfigSuccess(response.data));
setIsLoading(false);
return response.data;
} catch (err: any) {
const errorMessage = err.response?.data?.detail || err.message || 'Failed to save configuration';
dispatch(setConfigError(errorMessage));
setError(errorMessage);
setIsLoading(false);
throw new Error(errorMessage);
}
};
const resetConfig = async () => {
setIsLoading(true);
setError(null);
try {
const response = await axios.post(`${URL}/api/v1/config/reset`);
dispatch(setConfigSuccess(response.data));
setIsLoading(false);
return response.data;
} catch (err: any) {
const errorMessage = err.response?.data?.detail || err.message || 'Failed to reset configuration';
dispatch(setConfigError(errorMessage));
setError(errorMessage);
setIsLoading(false);
throw new Error(errorMessage);
}
};
const saveLLMConfig = async (llmConfig: LLMProvider) => {
setIsLoading(true);
setError(null);
try {
const response = await axios.put(`${URL}/api/v1/config/mem0/llm`, llmConfig);
dispatch(updateLLM(response.data));
setIsLoading(false);
return response.data;
} catch (err: any) {
const errorMessage = err.response?.data?.detail || err.message || 'Failed to save LLM configuration';
setError(errorMessage);
setIsLoading(false);
throw new Error(errorMessage);
}
};
const saveEmbedderConfig = async (embedderConfig: EmbedderProvider) => {
setIsLoading(true);
setError(null);
try {
const response = await axios.put(`${URL}/api/v1/config/mem0/embedder`, embedderConfig);
dispatch(updateEmbedder(response.data));
setIsLoading(false);
return response.data;
} catch (err: any) {
const errorMessage = err.response?.data?.detail || err.message || 'Failed to save Embedder configuration';
setError(errorMessage);
setIsLoading(false);
throw new Error(errorMessage);
}
};
return {
fetchConfig,
saveConfig,
saveLLMConfig,
saveEmbedderConfig,
resetConfig,
isLoading,
error
};
};

View File

@@ -0,0 +1,114 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
export interface LLMConfig {
model: string;
temperature: number;
max_tokens: number;
api_key?: string;
ollama_base_url?: string;
}
export interface LLMProvider {
provider: string;
config: LLMConfig;
}
export interface EmbedderConfig {
model: string;
api_key?: string;
ollama_base_url?: string;
}
export interface EmbedderProvider {
provider: string;
config: EmbedderConfig;
}
export interface Mem0Config {
llm?: LLMProvider;
embedder?: EmbedderProvider;
}
export interface OpenMemoryConfig {
custom_instructions?: string | null;
}
export interface ConfigState {
openmemory: OpenMemoryConfig;
mem0: Mem0Config;
status: 'idle' | 'loading' | 'succeeded' | 'failed';
error: string | null;
}
const initialState: ConfigState = {
openmemory: {
custom_instructions: null,
},
mem0: {
llm: {
provider: 'openai',
config: {
model: 'gpt-4o-mini',
temperature: 0.1,
max_tokens: 2000,
api_key: 'env:OPENAI_API_KEY',
},
},
embedder: {
provider: 'openai',
config: {
model: 'text-embedding-3-small',
api_key: 'env:OPENAI_API_KEY',
},
},
},
status: 'idle',
error: null,
};
const configSlice = createSlice({
name: 'config',
initialState,
reducers: {
setConfigLoading: (state) => {
state.status = 'loading';
state.error = null;
},
setConfigSuccess: (state, action: PayloadAction<{ openmemory?: OpenMemoryConfig; mem0: Mem0Config }>) => {
if (action.payload.openmemory) {
state.openmemory = action.payload.openmemory;
}
state.mem0 = action.payload.mem0;
state.status = 'succeeded';
state.error = null;
},
setConfigError: (state, action: PayloadAction<string>) => {
state.status = 'failed';
state.error = action.payload;
},
updateOpenMemory: (state, action: PayloadAction<OpenMemoryConfig>) => {
state.openmemory = action.payload;
},
updateLLM: (state, action: PayloadAction<LLMProvider>) => {
state.mem0.llm = action.payload;
},
updateEmbedder: (state, action: PayloadAction<EmbedderProvider>) => {
state.mem0.embedder = action.payload;
},
updateMem0Config: (state, action: PayloadAction<Mem0Config>) => {
state.mem0 = action.payload;
},
},
});
export const {
setConfigLoading,
setConfigSuccess,
setConfigError,
updateOpenMemory,
updateLLM,
updateEmbedder,
updateMem0Config,
} = configSlice.actions;
export default configSlice.reducer;

View File

@@ -4,6 +4,7 @@ import profileReducer from './profileSlice';
import appsReducer from './appsSlice';
import uiReducer from './uiSlice';
import filtersReducer from './filtersSlice';
import configReducer from './configSlice';
export const store = configureStore({
reducer: {
@@ -12,6 +13,7 @@ export const store = configureStore({
apps: appsReducer,
ui: uiReducer,
filters: filtersReducer,
config: configReducer,
},
});