+ );
+}
diff --git a/webapp/components/common/Explorer/index.tsx b/webapp/components/common/Explorer/index.tsx
new file mode 100644
index 00000000..c8deecf0
--- /dev/null
+++ b/webapp/components/common/Explorer/index.tsx
@@ -0,0 +1,46 @@
+// Copyright 2024 mik
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import useTranslation from '@/hooks/useTranslation';
+import ExplorerList from './ExplorerList';
+
+export type ExplorerProps = {
+ icon?: React.ReactNode;
+ title?: string;
+ children?: React.ReactNode;
+ toolbar?: React.ReactNode;
+};
+
+export default function Explorer({ icon, title = '', children, toolbar }: ExplorerProps) {
+ const { t } = useTranslation();
+
+ return (
+
+
+
+ );
+}
+
+export { ExplorerList };
diff --git a/webapp/components/common/RecordView/Header.tsx b/webapp/components/common/RecordView/Header.tsx
new file mode 100644
index 00000000..ae1a5093
--- /dev/null
+++ b/webapp/components/common/RecordView/Header.tsx
@@ -0,0 +1,47 @@
+// Copyright 2024 mik
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// Copyright 2023 Mik Bry
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+export type ToolbarProps = {
+ title: string | React.ReactNode;
+ toolbar?: React.ReactNode;
+};
+
+export default function Header({ title, toolbar }: ToolbarProps) {
+ return (
+
+
+
+ {typeof title === 'string' && (
+ {title}
+ )}
+ {typeof title !== 'string' && title}
+
+
{toolbar}
+
+
+ );
+}
diff --git a/webapp/components/common/RecordView/index.tsx b/webapp/components/common/RecordView/index.tsx
new file mode 100644
index 00000000..a91212f4
--- /dev/null
+++ b/webapp/components/common/RecordView/index.tsx
@@ -0,0 +1,37 @@
+// Copyright 2024 mik
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// import useTranslation from '@/hooks/useTranslation';
+import Header from './Header';
+
+export type RecordViewProps = {
+ title: string | React.ReactNode;
+ selectedId?: string;
+ children: React.ReactNode;
+ toolbar?: React.ReactNode;
+};
+
+export default function RecordView({ title, selectedId, children, toolbar }: RecordViewProps) {
+ // const { t } = useTranslation();
+ return (
+
+
+
+
+ {children}
+
+
+
+ );
+}
diff --git a/webapp/components/common/Sidebar/index.tsx b/webapp/components/common/Sidebar/index.tsx
index 2946c115..c4bec6b7 100644
--- a/webapp/components/common/Sidebar/index.tsx
+++ b/webapp/components/common/Sidebar/index.tsx
@@ -17,6 +17,7 @@
import { useContext } from 'react';
import Image from 'next/image';
import {
+ Bot,
BrainCircuit,
Keyboard,
LucideIcon,
@@ -52,6 +53,13 @@ const sidebarItems: Array = [
icon: MessagesSquare,
shortcut: ShortcutIds.DISPLAY_THREADS,
},
+ {
+ name: 'Assistants',
+ href: Ui.Page.Assistants,
+ page: Ui.Page.Assistants,
+ icon: Bot,
+ shortcut: ShortcutIds.DISPLAY_ASSISTANTS,
+ },
{
name: 'Models',
href: Ui.Page.Models,
diff --git a/webapp/components/providers/Provider.tsx b/webapp/components/providers/Provider.tsx
index 9c49f910..78e9a7c3 100644
--- a/webapp/components/providers/Provider.tsx
+++ b/webapp/components/providers/Provider.tsx
@@ -27,7 +27,7 @@ import OplaActions from './opla/Actions';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '../ui/tabs';
import { ScrollArea } from '../ui/scroll-area';
-function ProviderConfiguration({ providerId }: { providerId?: string }) {
+function ProviderView({ providerId }: { providerId?: string }) {
const { t } = useTranslation();
const { provider, hasParametersChanged, onParametersSave, onParameterChange, onProviderToggle } =
@@ -145,4 +145,4 @@ function ProviderConfiguration({ providerId }: { providerId?: string }) {
);
}
-export default ProviderConfiguration;
+export default ProviderView;
diff --git a/webapp/components/threads/Presets.tsx b/webapp/components/threads/Presets.tsx
index 8d8e29df..b14df79a 100644
--- a/webapp/components/threads/Presets.tsx
+++ b/webapp/components/threads/Presets.tsx
@@ -57,7 +57,7 @@ export default function Presets({
const compatibles = getCompatiblePresets(presets, model, provider);
const duplicatePreset = (p: Preset, newName?: string) => {
- const { id, name, readOnly, ...rest } = p;
+ const { id, name, readonly, ...rest } = p;
const template = deepMerge(rest, presetProperties);
const newPreset = createPreset(newName || `${p.id}-copy`, p.parentId || id, template);
setPresets([...presets, newPreset]);
@@ -136,7 +136,7 @@ export default function Presets({
{t('Duplicate selected preset')}
- {!preset?.readOnly && (
+ {!preset?.readonly && (
{
diff --git a/webapp/hooks/useAssistantStoreContext.tsx b/webapp/hooks/useAssistantStoreContext.tsx
new file mode 100644
index 00000000..8f5d4ba3
--- /dev/null
+++ b/webapp/hooks/useAssistantStoreContext.tsx
@@ -0,0 +1,13 @@
+// Copyright 2024 mik
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
diff --git a/webapp/hooks/useShortcuts.tsx b/webapp/hooks/useShortcuts.tsx
index af1aa2ac..376a2a50 100644
--- a/webapp/hooks/useShortcuts.tsx
+++ b/webapp/hooks/useShortcuts.tsx
@@ -26,6 +26,7 @@ export type KeyBinding = {
export enum ShortcutIds {
DISPLAY_THREADS = '#display-threads',
+ DISPLAY_ASSISTANTS = '#display-assistants',
DISPLAY_MODELS = '#display-models',
DISPLAY_PROVIDERS = '#display-providers',
DISPLAY_SETTINGS = '#display-settings',
@@ -55,10 +56,15 @@ export enum ShortcutIds {
// mod is the command key on mac, ctrl on windows/linux
export const defaultShortcuts: KeyBinding[] = [
{ command: ShortcutIds.DISPLAY_THREADS, keys: ['mod+1'], description: 'Display threads panel' },
- { command: ShortcutIds.DISPLAY_MODELS, keys: ['mod+2'], description: 'Display models panel' },
+ {
+ command: ShortcutIds.DISPLAY_ASSISTANTS,
+ keys: ['mod+2'],
+ description: 'Display assistants panel',
+ },
+ { command: ShortcutIds.DISPLAY_MODELS, keys: ['mod+3'], description: 'Display models panel' },
{
command: ShortcutIds.DISPLAY_PROVIDERS,
- keys: ['mod+3'],
+ keys: ['mod+4'],
description: 'Display providers panel',
},
{ command: ShortcutIds.DISPLAY_SETTINGS, keys: ['mod+t'], description: 'Toggle Settings' },
diff --git a/webapp/package.json b/webapp/package.json
index 23a6280b..251ca9bc 100644
--- a/webapp/package.json
+++ b/webapp/package.json
@@ -60,7 +60,8 @@
"unified": "^11.0.4",
"unist-util-visit": "^5.0.0",
"uuid": "^9.0.1",
- "zod": "^3.22.4"
+ "zod": "^3.22.4",
+ "zustand": "^4.5.1"
},
"devDependencies": {
"@tauri-apps/cli": "^1.5.9",
diff --git a/webapp/pages/assistants/[id].tsx b/webapp/pages/assistants/[id].tsx
new file mode 100644
index 00000000..610a64d4
--- /dev/null
+++ b/webapp/pages/assistants/[id].tsx
@@ -0,0 +1,24 @@
+// Copyright 2024 mik
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+'use client';
+
+import Assistants from '@/components/assistants';
+import { useRouter } from 'next/router';
+
+export default function DefaultAssistants() {
+ const router = useRouter();
+ const { id } = router.query;
+ return ;
+}
diff --git a/webapp/pages/assistants/index.tsx b/webapp/pages/assistants/index.tsx
new file mode 100644
index 00000000..c4b02713
--- /dev/null
+++ b/webapp/pages/assistants/index.tsx
@@ -0,0 +1,21 @@
+// Copyright 2024 mik
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+'use client';
+
+import Assistants from '@/components/assistants';
+
+export default function DefaultAssistants() {
+ return ;
+}
diff --git a/webapp/stores/assistants.ts b/webapp/stores/assistants.ts
new file mode 100644
index 00000000..78fbf74b
--- /dev/null
+++ b/webapp/stores/assistants.ts
@@ -0,0 +1,60 @@
+// Copyright 2024 mik
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { StateCreator } from 'zustand';
+import { Assistant } from '@/types';
+import { createBaseNamedRecord, updateRecord } from '@/utils/data';
+
+interface AssistantProps {
+ assistants: Assistant[];
+}
+
+export interface AssistantSlice extends AssistantProps {
+ getAssistant: (id: string | undefined) => Assistant | undefined;
+ createAssistant: (name: string, template?: Partial) => Assistant;
+ updateAssistant: (newAssistant: Assistant) => void;
+ deleteAssistant: (id: string) => void;
+}
+
+export type AssistantStore = ReturnType;
+
+const DEFAULT_PROPS: AssistantProps = {
+ assistants: [],
+};
+
+const createAssistantSlice =
+ (initProps?: Partial): StateCreator =>
+ (set, get) => ({
+ ...DEFAULT_PROPS,
+ ...initProps,
+ getAssistant: (id: string | undefined) => get().assistants.find((a) => a.id === id),
+ createAssistant: (name: string, template?: Partial) => {
+ const newAssistant = createBaseNamedRecord(name, template);
+ set((state: AssistantSlice) => ({ assistants: [...state.assistants, newAssistant] }));
+ return newAssistant;
+ },
+ updateAssistant: (newAssistant: Assistant) => {
+ const updatedAssistant: Assistant = updateRecord(newAssistant);
+ set((state: AssistantSlice) => ({
+ assistants: state.assistants.map((a) =>
+ a.id === updatedAssistant.id ? updatedAssistant : a,
+ ),
+ }));
+ },
+ deleteAssistant: (id: string) => {
+ set((state: AssistantSlice) => ({ assistants: state.assistants.filter((a) => a.id !== id) }));
+ },
+ });
+
+export default createAssistantSlice;
diff --git a/webapp/stores/index.ts b/webapp/stores/index.ts
new file mode 100644
index 00000000..7ff5a23d
--- /dev/null
+++ b/webapp/stores/index.ts
@@ -0,0 +1,33 @@
+// Copyright 2024 mik
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { create } from 'zustand';
+import { persist, createJSONStorage } from 'zustand/middleware';
+import { Assistant } from '@/types';
+import createAssistantSlice, { AssistantSlice } from './assistants';
+import Storage from './storage';
+
+export const useAssistantStore = create()(
+ persist(
+ (...a) => ({
+ ...createAssistantSlice()(...a),
+ }),
+ {
+ name: 'assistant',
+ storage: createJSONStorage(() => Storage, {}),
+ },
+ ),
+);
+
+export default useAssistantStore;
diff --git a/webapp/stores/storage.ts b/webapp/stores/storage.ts
new file mode 100644
index 00000000..6f490150
--- /dev/null
+++ b/webapp/stores/storage.ts
@@ -0,0 +1,46 @@
+// Copyright 2024 mik
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { StateStorage } from 'zustand/middleware';
+import dataStorage, { StorageType } from '@/utils/dataStorage';
+
+async function get(name: string): Promise {
+ const value = (await dataStorage(StorageType.TextFile).getItem(name)) as string;
+ return value;
+}
+
+async function set(name: string, value: string): Promise {
+ await dataStorage(StorageType.TextFile).setItem(name, value);
+}
+
+async function del(name: string): Promise {
+ await dataStorage(StorageType.TextFile).setItem(name, undefined);
+}
+
+// Custom storage object
+const storage: StateStorage = {
+ getItem: async (name: string): Promise =>
+ /* logger.info(name, 'has been retrieved'); */
+ (await get(name)) || null,
+ setItem: async (name: string, value: string): Promise => {
+ // logger.info(name, 'with value', value, 'has been saved');
+ await set(name, value);
+ },
+ removeItem: async (name: string): Promise => {
+ // logger.info(name, 'has been deleted');
+ await del(name);
+ },
+};
+
+export default storage;
diff --git a/webapp/types/index.ts b/webapp/types/index.ts
index 9115f14b..09ba8604 100644
--- a/webapp/types/index.ts
+++ b/webapp/types/index.ts
@@ -110,7 +110,7 @@ export type Preset = BaseNamedRecord & {
provider?: string;
models?: string[];
- readOnly?: boolean;
+ readonly?: boolean;
system?: string;
parameters?: Record;
@@ -346,6 +346,25 @@ export type ModelsConfiguration = {
items: Array;
};
+export type AssistantTarget = {
+ id: string;
+ parent?: string;
+ enabled?: boolean;
+ models?: string[];
+ provider?: string;
+
+ system?: string;
+ parameters?: Record;
+ contextWindowPolicy?: ContextWindowPolicy;
+ keepSystem?: boolean;
+};
+export type Assistant = BaseNamedRecord & {
+ disabled?: boolean;
+ parent?: string;
+ readonly?: boolean;
+ targets?: AssistantTarget[];
+};
+
export type Store = {
settings: Settings;
server: ServerConfiguration;
diff --git a/webapp/types/ui.ts b/webapp/types/ui.ts
index 1f1224e6..cda6122b 100644
--- a/webapp/types/ui.ts
+++ b/webapp/types/ui.ts
@@ -63,6 +63,7 @@ export enum ViewName {
export enum Page {
Threads = '/threads',
Archives = '/archives',
+ Assistants = '/assistants',
Models = '/models',
Providers = '/providers',
}
diff --git a/webapp/utils/data/index.ts b/webapp/utils/data/index.ts
index 8d6665d4..6dada8bc 100644
--- a/webapp/utils/data/index.ts
+++ b/webapp/utils/data/index.ts
@@ -23,19 +23,18 @@ const createBaseRecord = () => {
return item as T;
};
-const updateRecord = (item: BaseIdRecord) => ({
- ...item,
- updatedAt: Date.now(),
-});
+const updateRecord = (item: BaseIdRecord) =>
+ ({
+ ...item,
+ updatedAt: Date.now(),
+ }) as T;
-const createBaseNamedRecord = (name: string, description?: string): T => {
+const createBaseNamedRecord = (name: string, template?: Partial): T => {
const item: BaseNamedRecord = {
+ ...template,
...createBaseRecord(),
name,
};
- if (description) {
- item.description = description;
- }
return item as T;
};
diff --git a/webapp/utils/data/presets.ts b/webapp/utils/data/presets.ts
index 4f5c6bb5..2c4306e7 100644
--- a/webapp/utils/data/presets.ts
+++ b/webapp/utils/data/presets.ts
@@ -19,14 +19,14 @@ export const defaultPresets: Preset[] = [
{
id: 'opla',
name: 'Opla',
- readOnly: true,
+ readonly: true,
updatedAt: 0,
createdAt: 0,
},
{
id: 'openai',
name: 'OpenAI',
- readOnly: true,
+ readonly: true,
updatedAt: 0,
createdAt: 0,
},
@@ -34,7 +34,7 @@ export const defaultPresets: Preset[] = [
id: 'gpt-3.5',
parentId: 'openai',
name: 'ChatGPT-3.5',
- readOnly: true,
+ readonly: true,
updatedAt: 0,
createdAt: 0,
},
@@ -42,7 +42,7 @@ export const defaultPresets: Preset[] = [
id: 'gpt-4',
parentId: 'openai',
name: 'ChatGPT-4',
- readOnly: true,
+ readonly: true,
updatedAt: 0,
createdAt: 0,
},
@@ -113,7 +113,7 @@ export const getCompatiblePresets = (presets: Preset[], model?: string, provider
}
});
presets.forEach((p) => {
- if (p.parentId && compatiblePresets[p.parentId] && !p.readOnly) {
+ if (p.parentId && compatiblePresets[p.parentId] && !p.readonly) {
compatiblePresets[p.id] = true;
}
});
diff --git a/webapp/utils/dataStorage.ts b/webapp/utils/dataStorage.ts
index 5adc649f..33363539 100644
--- a/webapp/utils/dataStorage.ts
+++ b/webapp/utils/dataStorage.ts
@@ -16,8 +16,9 @@ import { toast } from 'sonner';
import logger from './logger';
import { deleteDir, deleteFile, readTextFile, writeTextFile } from './backend/tauri';
-enum StorageType {
- File,
+export enum StorageType {
+ JSON,
+ TextFile,
LocalStorage,
}
@@ -28,7 +29,7 @@ type DataStorage = {
const createPathAndJsonFile = (key: string, path: string) => `${path}/${key}.json`;
-const readFromLocalStorage = async (key: string, path: string) => {
+const readFromLocalStorage = async (key: string, path: string, json = true) => {
let text: string;
try {
text = await readTextFile(createPathAndJsonFile(key, path));
@@ -37,6 +38,9 @@ const readFromLocalStorage = async (key: string, path: string) => {
return null;
}
+ if (!json) {
+ return text as T;
+ }
try {
return JSON.parse(text) as T;
} catch (e) {
@@ -46,8 +50,12 @@ const readFromLocalStorage = async (key: string, path: string) => {
return null;
};
-const writeToLocalStorage = async (key: string, value: T, path: string) => {
- await writeTextFile(createPathAndJsonFile(key, path), JSON.stringify(value, null, 2), true);
+const writeToLocalStorage = async (key: string, value: T, path: string, json = true) => {
+ await writeTextFile(
+ createPathAndJsonFile(key, path),
+ json ? JSON.stringify(value, null, 2) : (value as string),
+ true,
+ );
};
const deleteFromLocalStorage = async (key: string, path: string) => {
@@ -95,16 +103,10 @@ const MockStorage: DataStorage = {
},
};
-const FileStorage: DataStorage = {
+const JsonStorage: DataStorage = {
async getItem(key: string, defaultValue?: T, path = '') {
- logger.warn('FileStorage.getItem() called', path);
+ // logger.warn('JsonStorage.getItem() called', path);
const value = await readFromLocalStorage(key, path);
- /* if (!value || (Array.isArray(value) && value.length === 0)) {
- value = await LocalStorage.getItem(key, defaultValue);
- if (value || defaultValue) {
- await writeToLocalStorage(key, value || defaultValue, path);
- }
- } */
return value || (defaultValue as T);
},
async setItem(key: string, value: T, path = '') {
@@ -115,11 +117,28 @@ const FileStorage: DataStorage = {
},
};
-const persistentStorage = (type = StorageType.File): DataStorage => {
+const TextFileStorage: DataStorage = {
+ async getItem(key: string, defaultValue?: T, path = '') {
+ // logger.warn('TextFileStorage.getItem() called', path);
+ const value = await readFromLocalStorage(key, path, false);
+ return value || (defaultValue as T);
+ },
+ async setItem(key: string, value: T, path = '') {
+ if (value === undefined) {
+ return deleteFromLocalStorage(key, path);
+ }
+ // logger.info('TextFileStorage.setItem()', key, value, path);
+ return writeToLocalStorage(key, value, path, false);
+ },
+};
+
+const persistentStorage = (type = StorageType.JSON): DataStorage => {
if (window && window.localStorage) {
switch (type) {
- case StorageType.File:
- return FileStorage;
+ case StorageType.JSON:
+ return JsonStorage;
+ case StorageType.TextFile:
+ return TextFileStorage;
case StorageType.LocalStorage:
return LocalStorage;
default: