diff --git a/packages/backend/src/managers/applicationManager.ts b/packages/backend/src/managers/applicationManager.ts index 8f3c61986..4bf23d8e1 100644 --- a/packages/backend/src/managers/applicationManager.ts +++ b/packages/backend/src/managers/applicationManager.ts @@ -1,3 +1,21 @@ +/********************************************************************** + * Copyright (C) 2024 Red Hat, Inc. + * + * 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. + * + * SPDX-License-Identifier: Apache-2.0 + ***********************************************************************/ + import type { Recipe } from '@shared/src/models/IRecipe'; import { arch } from 'node:os'; import type { GitManager } from './gitManager'; diff --git a/packages/backend/src/managers/catalogManager.ts b/packages/backend/src/managers/catalogManager.ts new file mode 100644 index 000000000..776bf27da --- /dev/null +++ b/packages/backend/src/managers/catalogManager.ts @@ -0,0 +1,119 @@ +/********************************************************************** + * Copyright (C) 2024 Red Hat, Inc. + * + * 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. + * + * SPDX-License-Identifier: Apache-2.0 + ***********************************************************************/ + +import type { Catalog } from '@shared/src/models/ICatalog'; +import path from 'node:path'; +import { existsSync, promises } from 'node:fs'; +import defaultCatalog from '../ai.json'; +import type { Category } from '@shared/src/models/ICategory'; +import type { Recipe } from '@shared/src/models/IRecipe'; +import type { ModelInfo } from '@shared/src/models/IModelInfo'; +import { MSG_NEW_CATALOG_STATE } from '@shared/Messages'; +import { fs } from '@podman-desktop/api'; +import type { Webview } from '@podman-desktop/api'; + +export class CatalogManager { + private catalog: Catalog; + + constructor( + private appUserDirectory: string, + private webview: Webview, + ) { + // We start with an empty catalog, for the methods to work before the catalog is loaded + this.catalog = { + categories: [], + models: [], + recipes: [], + }; + } + + public getCatalog(): Catalog { + return this.catalog; + } + + public getCategories(): Category[] { + return this.catalog.categories; + } + + public getModels(): ModelInfo[] { + return this.catalog.models; + } + public getRecipes(): Recipe[] { + return this.catalog.recipes; + } + + async loadCatalog() { + const catalogPath = path.resolve(this.appUserDirectory, 'catalog.json'); + if (!existsSync(catalogPath)) { + return this.setCatalog(defaultCatalog); + } + + try { + this.watchCatalogFile(catalogPath); // do not await, we want to do this async + } catch (err: unknown) { + console.error("unable to watch catalog file, changes to the catalog file won't be reflected to the UI", err); + } + + try { + const cat = await this.readAndAnalyzeCatalog(catalogPath); + return this.setCatalog(cat); + } catch (err: unknown) { + console.error('unable to read catalog file, reverting to default catalog', err); + } + // If something went wrong we load the default catalog + return this.setCatalog(defaultCatalog); + } + + watchCatalogFile(path: string) { + const watcher = fs.createFileSystemWatcher(path); + watcher.onDidCreate(async () => { + try { + const cat = await this.readAndAnalyzeCatalog(path); + await this.setCatalog(cat); + } catch (err: unknown) { + console.error('unable to read created catalog file, continue using default catalog', err); + } + }); + watcher.onDidDelete(async () => { + console.log('user catalog file deleted, reverting to default catalog'); + await this.setCatalog(defaultCatalog); + }); + watcher.onDidChange(async () => { + try { + const cat = await this.readAndAnalyzeCatalog(path); + await this.setCatalog(cat); + } catch (err: unknown) { + console.error('unable to read modified catalog file, reverting to default catalog', err); + } + }); + } + + async readAndAnalyzeCatalog(path: string): Promise { + const data = await promises.readFile(path, 'utf-8'); + return JSON.parse(data) as Catalog; + // TODO(feloy): check version, ... + } + + async setCatalog(newCatalog: Catalog) { + this.catalog = newCatalog; + await this.webview.postMessage({ + id: MSG_NEW_CATALOG_STATE, + body: this.catalog, + }); + } +} diff --git a/packages/backend/src/managers/gitManager.ts b/packages/backend/src/managers/gitManager.ts index a5004acb3..919e232ac 100644 --- a/packages/backend/src/managers/gitManager.ts +++ b/packages/backend/src/managers/gitManager.ts @@ -1,3 +1,21 @@ +/********************************************************************** + * Copyright (C) 2024 Red Hat, Inc. + * + * 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. + * + * SPDX-License-Identifier: Apache-2.0 + ***********************************************************************/ + import simpleGit, { type SimpleGit } from 'simple-git'; export class GitManager { diff --git a/packages/backend/src/playground.ts b/packages/backend/src/managers/playground.ts similarity index 85% rename from packages/backend/src/playground.ts rename to packages/backend/src/managers/playground.ts index 214c9349d..7bf9e8c38 100644 --- a/packages/backend/src/playground.ts +++ b/packages/backend/src/managers/playground.ts @@ -1,3 +1,21 @@ +/********************************************************************** + * Copyright (C) 2024 Red Hat, Inc. + * + * 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. + * + * SPDX-License-Identifier: Apache-2.0 + ***********************************************************************/ + import { provider, containerEngine, @@ -10,10 +28,11 @@ import type { ModelResponse } from '@shared/src/models/IModelResponse'; import path from 'node:path'; import * as http from 'node:http'; -import { getFreePort } from './utils/ports'; +import { getFreePort } from '../utils/ports'; import type { QueryState } from '@shared/src/models/IPlaygroundQueryState'; import { MSG_NEW_PLAYGROUND_QUERIES_STATE } from '@shared/Messages'; +// TODO: this should not be hardcoded const LOCALAI_IMAGE = 'quay.io/go-skynet/local-ai:v2.5.1'; function findFirstProvider(): ProviderContainerConnection | undefined { diff --git a/packages/backend/src/models/AIConfig.ts b/packages/backend/src/models/AIConfig.ts index 097f5230d..849a549ce 100644 --- a/packages/backend/src/models/AIConfig.ts +++ b/packages/backend/src/models/AIConfig.ts @@ -1,3 +1,21 @@ +/********************************************************************** + * Copyright (C) 2024 Red Hat, Inc. + * + * 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. + * + * SPDX-License-Identifier: Apache-2.0 + ***********************************************************************/ + import * as jsYaml from 'js-yaml'; export interface ContainerConfig { diff --git a/packages/backend/src/registries/RecipeStatusRegistry.ts b/packages/backend/src/registries/RecipeStatusRegistry.ts index 63820c2f9..0a8779817 100644 --- a/packages/backend/src/registries/RecipeStatusRegistry.ts +++ b/packages/backend/src/registries/RecipeStatusRegistry.ts @@ -1,3 +1,21 @@ +/********************************************************************** + * Copyright (C) 2024 Red Hat, Inc. + * + * 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. + * + * SPDX-License-Identifier: Apache-2.0 + ***********************************************************************/ + import type { RecipeStatus } from '@shared/src/models/IRecipeStatus'; import type { TaskRegistry } from './TaskRegistry'; diff --git a/packages/backend/src/registries/TaskRegistry.ts b/packages/backend/src/registries/TaskRegistry.ts index 1508a831c..7c7f1cf6e 100644 --- a/packages/backend/src/registries/TaskRegistry.ts +++ b/packages/backend/src/registries/TaskRegistry.ts @@ -1,3 +1,21 @@ +/********************************************************************** + * Copyright (C) 2024 Red Hat, Inc. + * + * 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. + * + * SPDX-License-Identifier: Apache-2.0 + ***********************************************************************/ + import type { Task } from '@shared/src/models/ITask'; export class TaskRegistry { diff --git a/packages/backend/src/studio-api-impl.spec.ts b/packages/backend/src/studio-api-impl.spec.ts index 408ad551b..71c31bbd6 100644 --- a/packages/backend/src/studio-api-impl.spec.ts +++ b/packages/backend/src/studio-api-impl.spec.ts @@ -24,11 +24,12 @@ import userContent from './ai-user-test.json'; import type { ApplicationManager } from './managers/applicationManager'; import type { RecipeStatusRegistry } from './registries/RecipeStatusRegistry'; import { StudioApiImpl } from './studio-api-impl'; -import type { PlayGroundManager } from './playground'; +import type { PlayGroundManager } from './managers/playground'; import type { TaskRegistry } from './registries/TaskRegistry'; import type { Webview } from '@podman-desktop/api'; import * as fs from 'node:fs'; +import { CatalogManager } from './managers/catalogManager'; vi.mock('./ai.json', () => { return { @@ -36,19 +37,47 @@ vi.mock('./ai.json', () => { }; }); +vi.mock('node:fs', () => { + return { + existsSync: vi.fn(), + promises: { + readFile: vi.fn(), + }, + }; +}); + +vi.mock('@podman-desktop/api', () => { + return { + fs: { + createFileSystemWatcher: () => ({ + onDidCreate: vi.fn(), + onDidDelete: vi.fn(), + onDidChange: vi.fn(), + }), + }, + }; +}); + let studioApiImpl: StudioApiImpl; +let catalogManager; beforeEach(async () => { + const appUserDirectory = '.'; + + // Creating CatalogManager + catalogManager = new CatalogManager(appUserDirectory, { + postMessage: vi.fn(), + } as unknown as Webview); + + // Creating StudioApiImpl studioApiImpl = new StudioApiImpl( { - appUserDirectory: '.', + appUserDirectory, } as unknown as ApplicationManager, {} as unknown as RecipeStatusRegistry, {} as unknown as TaskRegistry, {} as unknown as PlayGroundManager, - { - postMessage: vi.fn(), - } as unknown as Webview, + catalogManager, ); vi.resetAllMocks(); vi.mock('node:fs'); @@ -57,11 +86,11 @@ beforeEach(async () => { describe('invalid user catalog', () => { beforeEach(async () => { vi.spyOn(fs.promises, 'readFile').mockResolvedValue('invalid json'); - await studioApiImpl.loadCatalog(); + await catalogManager.loadCatalog(); }); - test('expect correct model is returned with valid id', () => { - const model = studioApiImpl.getModelById('llama-2-7b-chat.Q5_K_S'); + test('expect correct model is returned with valid id', async () => { + const model = await studioApiImpl.getModelById('llama-2-7b-chat.Q5_K_S'); expect(model).toBeDefined(); expect(model.name).toEqual('Llama-2-7B-Chat-GGUF'); expect(model.registry).toEqual('Hugging Face'); @@ -70,15 +99,15 @@ describe('invalid user catalog', () => { ); }); - test('expect error if id does not correspond to any model', () => { - expect(() => studioApiImpl.getModelById('unknown')).toThrowError('No model found having id unknown'); + test('expect error if id does not correspond to any model', async () => { + await expect(() => studioApiImpl.getModelById('unknown')).rejects.toThrowError('No model found having id unknown'); }); }); test('expect correct model is returned from default catalog with valid id when no user catalog exists', async () => { vi.spyOn(fs, 'existsSync').mockReturnValue(false); - await studioApiImpl.loadCatalog(); - const model = studioApiImpl.getModelById('llama-2-7b-chat.Q5_K_S'); + await catalogManager.loadCatalog(); + const model = await studioApiImpl.getModelById('llama-2-7b-chat.Q5_K_S'); expect(model).toBeDefined(); expect(model.name).toEqual('Llama-2-7B-Chat-GGUF'); expect(model.registry).toEqual('Hugging Face'); @@ -90,8 +119,9 @@ test('expect correct model is returned from default catalog with valid id when n test('expect correct model is returned with valid id when the user catalog is valid', async () => { vi.spyOn(fs, 'existsSync').mockReturnValue(true); vi.spyOn(fs.promises, 'readFile').mockResolvedValue(JSON.stringify(userContent)); - await studioApiImpl.loadCatalog(); - const model = studioApiImpl.getModelById('model1'); + + await catalogManager.loadCatalog(); + const model = await studioApiImpl.getModelById('model1'); expect(model).toBeDefined(); expect(model.name).toEqual('Model 1'); expect(model.registry).toEqual('Hugging Face'); diff --git a/packages/backend/src/studio-api-impl.ts b/packages/backend/src/studio-api-impl.ts index 58e29f35e..73a73b16b 100644 --- a/packages/backend/src/studio-api-impl.ts +++ b/packages/backend/src/studio-api-impl.ts @@ -1,99 +1,51 @@ +/********************************************************************** + * Copyright (C) 2024 Red Hat, Inc. + * + * 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. + * + * SPDX-License-Identifier: Apache-2.0 + ***********************************************************************/ + import type { StudioAPI } from '@shared/src/StudioAPI'; +import type { Category } from '@shared/src/models/ICategory'; import type { Recipe } from '@shared/src/models/IRecipe'; -import defaultCatalog from './ai.json'; import type { ApplicationManager } from './managers/applicationManager'; import type { RecipeStatusRegistry } from './registries/RecipeStatusRegistry'; import type { RecipeStatus } from '@shared/src/models/IRecipeStatus'; import type { ModelInfo } from '@shared/src/models/IModelInfo'; import type { TaskRegistry } from './registries/TaskRegistry'; import type { Task } from '@shared/src/models/ITask'; -import type { PlayGroundManager } from './playground'; +import type { PlayGroundManager } from './managers/playground'; import * as podmanDesktopApi from '@podman-desktop/api'; import type { QueryState } from '@shared/src/models/IPlaygroundQueryState'; -import type { Catalog } from '@shared/src/models/ICatalog'; -import { MSG_NEW_CATALOG_STATE } from '@shared/Messages'; import * as path from 'node:path'; -import * as fs from 'node:fs'; +import type { CatalogManager } from './managers/catalogManager'; +import type { Catalog } from '@shared/src/models/ICatalog'; export const RECENT_CATEGORY_ID = 'recent-category'; export class StudioApiImpl implements StudioAPI { - private catalog: Catalog; - constructor( private applicationManager: ApplicationManager, private recipeStatusRegistry: RecipeStatusRegistry, private taskRegistry: TaskRegistry, private playgroundManager: PlayGroundManager, - private webview: podmanDesktopApi.Webview, - ) { - // We start with an empty catalog, for the methods to work before the catalog is loaded - this.catalog = { - categories: [], - models: [], - recipes: [], - }; - } - - async loadCatalog() { - const catalogPath = path.resolve(this.applicationManager.appUserDirectory, 'catalog.json'); - - try { - this.watchCatalogFile(catalogPath); // do not await, we want to do this async - } catch (err: unknown) { - console.error("unable to watch catalog file, changes to the catalog file won't be reflected to the UI", err); - } - - try { - if (!fs.existsSync(catalogPath)) { - await this.setCatalog(defaultCatalog); - return; - } - const cat = await this.readAndAnalyzeCatalog(catalogPath); - await this.setCatalog(cat); - } catch (err: unknown) { - console.error('unable to read catalog file, reverting to default catalog', err); - await this.setCatalog(defaultCatalog); - } - } + private catalogManager: CatalogManager, + ) {} - watchCatalogFile(path: string) { - const watcher = podmanDesktopApi.fs.createFileSystemWatcher(path); - watcher.onDidCreate(async () => { - try { - const cat = await this.readAndAnalyzeCatalog(path); - await this.setCatalog(cat); - } catch (err: unknown) { - console.error('unable to read created catalog file, continue using default catalog', err); - } - }); - watcher.onDidDelete(async () => { - console.log('user catalog file deleted, reverting to default catalog'); - await this.setCatalog(defaultCatalog); - }); - watcher.onDidChange(async () => { - try { - const cat = await this.readAndAnalyzeCatalog(path); - await this.setCatalog(cat); - } catch (err: unknown) { - console.error('unable to read modified catalog file, reverting to default catalog', err); - } - }); - } - - async readAndAnalyzeCatalog(path: string): Promise { - const data = await fs.promises.readFile(path, 'utf-8'); - return JSON.parse(data) as Catalog; - // TODO(feloy): check version, ... - } - - async setCatalog(newCatalog: Catalog) { - this.catalog = newCatalog; - await this.webview.postMessage({ - id: MSG_NEW_CATALOG_STATE, - body: this.catalog, - }); + async ping(): Promise { + return 'pong'; } async openURL(url: string): Promise { @@ -104,36 +56,53 @@ export class StudioApiImpl implements StudioAPI { return this.recipeStatusRegistry.getStatus(recipeId); } - async ping(): Promise { - return 'pong'; + async getRecentRecipes(): Promise { + return []; // no recent implementation for now } - async getCatalog(): Promise { - return this.catalog; + async getCategories(): Promise { + return this.catalogManager.getCategories(); + } + + async getRecipesByCategory(categoryId: string): Promise { + if (categoryId === RECENT_CATEGORY_ID) return this.getRecentRecipes(); + + // TODO: move logic to catalog manager + return this.catalogManager.getRecipes().filter(recipe => recipe.categories.includes(categoryId)); } - getRecipeById(recipeId: string): Recipe { - const recipe = (this.catalog.recipes as Recipe[]).find(recipe => recipe.id === recipeId); + async getRecipeById(recipeId: string): Promise { + // TODO: move logic to catalog manager + const recipe = this.catalogManager.getRecipes().find(recipe => recipe.id === recipeId); if (recipe) return recipe; throw new Error('Not found'); } - getModelById(modelId: string): ModelInfo { - const model = this.catalog.models.find(m => modelId === m.id); + async getModelById(modelId: string): Promise { + // TODO: move logic to catalog manager + const model = this.catalogManager.getModels().find(m => modelId === m.id); if (!model) { throw new Error(`No model found having id ${modelId}`); } return model; } + async getModelsByIds(ids: string[]): Promise { + // TODO: move logic to catalog manager + return this.catalogManager.getModels().filter(m => ids.includes(m.id)) ?? []; + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async searchRecipes(_query: string): Promise { + return []; // todo: not implemented + } + async pullApplication(recipeId: string): Promise { - console.log('StudioApiImpl pullApplication', recipeId); - const recipe: Recipe = this.getRecipeById(recipeId); - console.log('StudioApiImpl recipe', recipe); + const recipe: Recipe = await this.getRecipeById(recipeId); // the user should have selected one model, we use the first one for the moment const modelId = recipe.models[0]; - const model = this.getModelById(modelId); + const model = await this.getModelById(modelId); // Do not wait for the pull application, run it separately this.applicationManager.pullApplication(recipe, model).catch((error: unknown) => console.warn(error)); @@ -142,9 +111,10 @@ export class StudioApiImpl implements StudioAPI { } async getLocalModels(): Promise { + // TODO: move logic to catalog manager const local = this.applicationManager.getLocalModels(); const localIds = local.map(l => l.id); - return this.catalog.models.filter(m => localIds.includes(m.id)); + return this.catalogManager.getModels().filter(m => localIds.includes(m.id)); } async getTasksByLabel(label: string): Promise { @@ -173,4 +143,8 @@ export class StudioApiImpl implements StudioAPI { async getPlaygroundStates(): Promise { return this.playgroundManager.getState(); } + + async getCatalog(): Promise { + return this.catalogManager.getCatalog(); + } } diff --git a/packages/backend/src/studio.ts b/packages/backend/src/studio.ts index 4136d8cb0..15db733c0 100644 --- a/packages/backend/src/studio.ts +++ b/packages/backend/src/studio.ts @@ -18,7 +18,7 @@ import type { ExtensionContext, WebviewOptions, WebviewPanel } from '@podman-desktop/api'; import { Uri, window } from '@podman-desktop/api'; -import { RpcExtension } from '@shared/src/MessageProxy'; +import { RpcExtension } from '@shared/src/messages/MessageProxy'; import { StudioApiImpl } from './studio-api-impl'; import { ApplicationManager } from './managers/applicationManager'; import { GitManager } from './managers/gitManager'; @@ -26,7 +26,8 @@ import { RecipeStatusRegistry } from './registries/RecipeStatusRegistry'; import * as fs from 'node:fs'; import { TaskRegistry } from './registries/TaskRegistry'; -import { PlayGroundManager } from './playground'; +import { PlayGroundManager } from './managers/playground'; +import { CatalogManager } from './managers/catalogManager'; export class Studio { readonly #extensionContext: ExtensionContext; @@ -36,6 +37,7 @@ export class Studio { rpcExtension: RpcExtension; studioApi: StudioApiImpl; playgroundManager: PlayGroundManager; + catalogManager: CatalogManager; constructor(readonly extensionContext: ExtensionContext) { this.#extensionContext = extensionContext; @@ -93,14 +95,20 @@ export class Studio { const recipeStatusRegistry = new RecipeStatusRegistry(taskRegistry); const applicationManager = new ApplicationManager(gitManager, recipeStatusRegistry, this.#extensionContext); this.playgroundManager = new PlayGroundManager(this.#panel.webview); + // Create catalog manager, responsible for loading the catalog files and watching for changes + this.catalogManager = new CatalogManager(applicationManager.appUserDirectory, this.#panel.webview); + + // Creating StudioApiImpl this.studioApi = new StudioApiImpl( applicationManager, recipeStatusRegistry, taskRegistry, this.playgroundManager, - this.#panel.webview, + this.catalogManager, ); - await this.studioApi.loadCatalog(); + + await this.catalogManager.loadCatalog(); + // Register the instance this.rpcExtension.registerInstance(StudioApiImpl, this.studioApi); } diff --git a/packages/backend/src/utils/pathUtils.ts b/packages/backend/src/utils/pathUtils.ts index 0d60707d3..f3fb60d6b 100644 --- a/packages/backend/src/utils/pathUtils.ts +++ b/packages/backend/src/utils/pathUtils.ts @@ -1,3 +1,21 @@ +/********************************************************************** + * Copyright (C) 2024 Red Hat, Inc. + * + * 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. + * + * SPDX-License-Identifier: Apache-2.0 + ***********************************************************************/ + import path from 'path'; export function getParentDirectory(filePath: string): string { diff --git a/packages/backend/src/utils/recipeStatusUtils.ts b/packages/backend/src/utils/recipeStatusUtils.ts index e04c0cbca..4e937c0bc 100644 --- a/packages/backend/src/utils/recipeStatusUtils.ts +++ b/packages/backend/src/utils/recipeStatusUtils.ts @@ -1,3 +1,21 @@ +/********************************************************************** + * Copyright (C) 2024 Red Hat, Inc. + * + * 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. + * + * SPDX-License-Identifier: Apache-2.0 + ***********************************************************************/ + import type { RecipeStatus, RecipeStatusState } from '@shared/src/models/IRecipeStatus'; import type { Task, TaskState } from '@shared/src/models/ITask'; import type { RecipeStatusRegistry } from '../registries/RecipeStatusRegistry'; diff --git a/packages/frontend/src/pages/ModelPlayground.svelte b/packages/frontend/src/pages/ModelPlayground.svelte index ab4811b62..62907d27d 100644 --- a/packages/frontend/src/pages/ModelPlayground.svelte +++ b/packages/frontend/src/pages/ModelPlayground.svelte @@ -60,7 +60,7 @@ result = undefined; // do not display anything before we get a response from askPlayground // (we can receive a new queryState before the new QueryId) - queryId = -1; + queryId = -1; queryId = await studioClient.askPlayground(model.id, prompt); } @@ -73,8 +73,8 @@ rows="4" class="w-full p-2 outline-none text-sm bg-charcoal-800 rounded-sm text-gray-700 placeholder-gray-700" placeholder="Type your prompt here"> - -
+ +
diff --git a/packages/frontend/src/utils/client.ts b/packages/frontend/src/utils/client.ts index 71c97a9c9..93efd1cea 100644 --- a/packages/frontend/src/utils/client.ts +++ b/packages/frontend/src/utils/client.ts @@ -1,5 +1,5 @@ import type { StudioAPI } from '@shared/src/StudioAPI'; -import { RpcBrowser } from '@shared/src/MessageProxy'; +import { RpcBrowser } from '@shared/src/messages/MessageProxy'; export const RECENT_CATEGORY_ID = 'recent-category'; const podmanDesktopApi = acquirePodmanDesktopApi(); diff --git a/packages/shared/src/MessageProxy.spec.ts b/packages/shared/src/messages/MessageProxy.spec.ts similarity index 96% rename from packages/shared/src/MessageProxy.spec.ts rename to packages/shared/src/messages/MessageProxy.spec.ts index 595ce0fa6..656b9b9e6 100644 --- a/packages/shared/src/MessageProxy.spec.ts +++ b/packages/shared/src/messages/MessageProxy.spec.ts @@ -1,6 +1,5 @@ import { test, expect, beforeAll } from 'vitest'; import { RpcBrowser, RpcExtension } from './MessageProxy'; -import type { PodmanDesktopApi } from '../../../types/podman-desktop-api'; import type { Webview } from '@podman-desktop/api'; let webview: Webview; diff --git a/packages/shared/src/MessageProxy.ts b/packages/shared/src/messages/MessageProxy.ts similarity index 95% rename from packages/shared/src/MessageProxy.ts rename to packages/shared/src/messages/MessageProxy.ts index 6e5e777b6..e98e83c56 100644 --- a/packages/shared/src/MessageProxy.ts +++ b/packages/shared/src/messages/MessageProxy.ts @@ -17,7 +17,6 @@ ***********************************************************************/ /* eslint-disable @typescript-eslint/no-explicit-any */ -import type { PodmanDesktopApi } from '../../../types/podman-desktop-api'; import type { Webview } from '@podman-desktop/api'; export interface IMessage { @@ -40,6 +39,8 @@ export interface ISubscribedMessage { body: any; } +type UnaryRPC = (...args: unknown[]) => Promise; + export function isMessageRequest(content: unknown): content is IMessageRequest { return ( content !== undefined && content !== null && typeof content === 'object' && 'id' in content && 'channel' in content @@ -90,7 +91,7 @@ export class RpcExtension { }); } - registerInstance(classType: { new (...args: any[]): T }, instance: T) { + registerInstance>(classType: { new (...args: any[]): T }, instance: T) { const methodNames = Object.getOwnPropertyNames(classType.prototype).filter( name => name !== 'constructor' && typeof instance[name as keyof T] === 'function', ); @@ -156,8 +157,7 @@ export class RpcBrowser { getProxy(): T { // eslint-disable-next-line @typescript-eslint/no-this-alias const thisRef = this; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const proxyHandler: ProxyHandler = { + const proxyHandler: ProxyHandler = { get(target, prop, receiver) { if (typeof prop === 'string') { return (...args: unknown[]) => {