From c67781aaeff2d387cb7bb7605e0c48496520fa98 Mon Sep 17 00:00:00 2001 From: axel7083 <42176370+axel7083@users.noreply.github.com> Date: Wed, 10 Jan 2024 15:23:55 -0500 Subject: [PATCH 1/5] feat: setup communication between front and back --- packages/backend/src/ai.json | 35 ++++++ packages/backend/src/studio-api-impl.ts | 36 ++++++ packages/backend/src/studio.ts | 20 +-- packages/backend/tsconfig.json | 44 ++++--- packages/backend/vite.config.js | 1 + packages/frontend/src/App.svelte | 34 ++--- packages/frontend/src/utils/client.ts | 40 +----- packages/frontend/tsconfig.json | 3 +- packages/frontend/vite.config.js | 1 + packages/shared/MessageProxy.ts | 161 ++++++++++++++++++++++++ packages/shared/StudioAPI.ts | 18 +-- types/podman-desktop-api.d.ts | 4 +- 12 files changed, 294 insertions(+), 103 deletions(-) create mode 100644 packages/backend/src/ai.json create mode 100644 packages/backend/src/studio-api-impl.ts create mode 100644 packages/shared/MessageProxy.ts diff --git a/packages/backend/src/ai.json b/packages/backend/src/ai.json new file mode 100644 index 000000000..570219221 --- /dev/null +++ b/packages/backend/src/ai.json @@ -0,0 +1,35 @@ +{ + "recipes": [ + { + "id": "chatbot", + "description" : "Chat bot application", + "name" : "ChatBot", + "repository": "https://github.com/michaelclifford/locallm", + "categories": [ + "natural-language-processing" + ] + } + ], + "categories": [ + { + "id": "natural-language-processing", + "name": "Natural Language Processing", + "description" : "Models that work with text: classify, summarize, translate, or generate text." + }, + { + "id": "computer-vision", + "description" : "Process images, from classification to object detection and segmentation.", + "name" : "Computer Vision" + }, + { + "id": "audio", + "description" : "Recognize speech or classify audio with audio models.", + "name" : "Audio" + }, + { + "id": "multimodal", + "description" : "Stuff about multimodal models goes here omg yes amazing.", + "name" : "Multimodal" + } + ] +} diff --git a/packages/backend/src/studio-api-impl.ts b/packages/backend/src/studio-api-impl.ts new file mode 100644 index 000000000..a03a0d126 --- /dev/null +++ b/packages/backend/src/studio-api-impl.ts @@ -0,0 +1,36 @@ +import type { StudioAPI } from '@shared/StudioAPI'; +import { Category } from '@shared/models/ICategory'; +import { Recipe } from '@shared/models/IRecipe'; +import content from './ai.json'; + +export const RECENT_CATEGORY_ID = 'recent-category'; + +export class StudioApiImpl implements StudioAPI { + async ping(): Promise { + return 'pong'; + } + + async getRecentRecipes(): Promise { + return content.recipes.toSpliced(0, 10); + } + + async getCategories(): Promise { + return content.categories; + } + + async getRecipesByCategory(categoryId: string): Promise { + if (categoryId === RECENT_CATEGORY_ID) return this.getRecentRecipes(); + + return content.recipes.filter(recipe => recipe.categories.includes(categoryId)); + } + + async getRecipeById(recipeId: string): Promise { + const recipe = (content.recipes as Recipe[]).find(recipe => recipe.id === recipeId); + if (recipe) return recipe; + throw new Error('Not found'); + } + + async searchRecipes(query: string): Promise { + return []; // todo: not implemented + } +} diff --git a/packages/backend/src/studio.ts b/packages/backend/src/studio.ts index 8ee6d7ad6..85d6dd1ef 100644 --- a/packages/backend/src/studio.ts +++ b/packages/backend/src/studio.ts @@ -19,12 +19,17 @@ import type { ExtensionContext, WebviewOptions, WebviewPanel } from '@podman-desktop/api'; import { Uri, window } from '@podman-desktop/api'; import { promises } from 'node:fs'; +import { RpcExtension } from '@shared/MessageProxy'; +import { StudioApiImpl } from './studio-api-impl'; export class Studio { readonly #extensionContext: ExtensionContext; #panel: WebviewPanel | undefined; + rpcExtension: RpcExtension; + studioApi: StudioApiImpl; + constructor(readonly extensionContext: ExtensionContext) { this.#extensionContext = extensionContext; } @@ -36,7 +41,6 @@ export class Studio { // register webview this.#panel = window.createWebviewPanel('studio', 'Studio extension', this.getWebviewOptions(extensionUri)); - this.#extensionContext.subscriptions.push(this.#panel); // update html @@ -74,15 +78,11 @@ export class Studio { this.#panel.webview.html = indexHtml; - // send a message to the webview 10s after it is created - setTimeout(() => { - this.#panel?.webview.postMessage({ command: 'hello' }); - }, 10000); - - // handle messages from the webview - this.#panel.webview.onDidReceiveMessage(message => { - console.log('received message from webview', message); - }); + // Let's create the api that the front will be able to call + this.rpcExtension = new RpcExtension(this.#panel.webview); + this.studioApi = new StudioApiImpl(); + // Register the instance + this.rpcExtension.registerInstance(StudioApiImpl, this.studioApi); } public async deactivate(): Promise { diff --git a/packages/backend/tsconfig.json b/packages/backend/tsconfig.json index bbda357e4..f02e19ee7 100644 --- a/packages/backend/tsconfig.json +++ b/packages/backend/tsconfig.json @@ -5,33 +5,39 @@ "moduleResolution": "Node", "resolveJsonModule": true, "lib": [ - "ES2017", - "webworker" + "ES2017", + "webworker" ], "sourceMap": true, "rootDir": "src", "outDir": "dist", + "allowSyntheticDefaultImports": true, "skipLibCheck": true, "types": [ - "node", - ] -}, -"include": [ + "node", + ], + "paths": { + "@shared/*": ["../shared/*"] + } + }, + "include": [ "src", "types/*.d.ts", - "../../types/**/*.d.ts" -], -"ts-node": { - "compilerOptions": { - "module": "CommonJS", - "lib": [ - "ES2020", - "DOM" - ], - "types": [ - "node" - ] + "../../types/**/*.d.ts", + "../shared/*.ts", + "../shared/**/*.ts" + ], + "ts-node": { + "compilerOptions": { + "module": "CommonJS", + "lib": [ + "ES2020", + "DOM" + ], + "types": [ + "node" + ] + } } -} } diff --git a/packages/backend/vite.config.js b/packages/backend/vite.config.js index 60aef55a5..911a37dce 100644 --- a/packages/backend/vite.config.js +++ b/packages/backend/vite.config.js @@ -33,6 +33,7 @@ const config = { alias: { '/@/': join(PACKAGE_ROOT, 'src') + '/', '/@gen/': join(PACKAGE_ROOT, 'src-generated') + '/', + '@shared/': join(PACKAGE_ROOT, '../shared') + '/', }, }, build: { diff --git a/packages/frontend/src/App.svelte b/packages/frontend/src/App.svelte index 211cbc7df..639f00e5a 100644 --- a/packages/frontend/src/App.svelte +++ b/packages/frontend/src/App.svelte @@ -1,36 +1,18 @@ diff --git a/packages/frontend/src/utils/client.ts b/packages/frontend/src/utils/client.ts index 453dc71fb..113d93a7f 100644 --- a/packages/frontend/src/utils/client.ts +++ b/packages/frontend/src/utils/client.ts @@ -1,40 +1,8 @@ -import type { Recipe } from '@shared/models/IRecipe'; -import type { Category } from '@shared/models/ICategory'; -import content from './ai.json'; import type { StudioAPI } from '@shared/StudioAPI'; +import { RpcBrowser } from '@shared/MessageProxy'; export const RECENT_CATEGORY_ID = 'recent-category'; +const podmanDesktopApi = acquirePodmanDesktopApi(); +const rpcBrowser: RpcBrowser = new RpcBrowser(window, podmanDesktopApi); -export class StudioClient implements StudioAPI { - // podmanDesktopApi = acquirePodmanDesktopApi?.(); - - async ping(): Promise { - return 'pong'; - } - - async getRecentRecipes(): Promise { - return content.recipes.toSpliced(0, 10); - } - - async getCategories(): Promise { - return content.categories; - } - - async getRecipesByCategory(categoryId: string): Promise { - if (categoryId === RECENT_CATEGORY_ID) return this.getRecentRecipes(); - - return content.recipes.filter(recipe => recipe.categories.includes(categoryId)); - } - - async getRecipeById(recipeId: string): Promise { - const recipe = (content.recipes as Recipe[]).find(recipe => recipe.id === recipeId); - if (recipe) return recipe; - throw new Error('Not found'); - } - - async searchRecipes(query: string): Promise { - return []; // todo: not implemented - } -} - -export const studioClient = new StudioClient(); +export const studioClient: StudioAPI = rpcBrowser.getProxy(); diff --git a/packages/frontend/tsconfig.json b/packages/frontend/tsconfig.json index d5f4b68e1..5432db6cc 100644 --- a/packages/frontend/tsconfig.json +++ b/packages/frontend/tsconfig.json @@ -25,6 +25,7 @@ "src/**/*.ts", "src/**/*.js", "src/**/*.svelte", - "../../types/**/*.d.ts" + "../../types/**/*.d.ts", + "../shared/**/*.ts" ] } diff --git a/packages/frontend/vite.config.js b/packages/frontend/vite.config.js index 2fc17c669..43007e85d 100644 --- a/packages/frontend/vite.config.js +++ b/packages/frontend/vite.config.js @@ -33,6 +33,7 @@ export default defineConfig({ resolve: { alias: { '/@/': join(PACKAGE_ROOT, 'src') + '/', + '@shared/': join(PACKAGE_ROOT, '../shared') + '/', }, }, plugins: [svelte({ hot: !process.env.VITEST })], diff --git a/packages/shared/MessageProxy.ts b/packages/shared/MessageProxy.ts new file mode 100644 index 000000000..197ec367f --- /dev/null +++ b/packages/shared/MessageProxy.ts @@ -0,0 +1,161 @@ +import type { PodmanDesktopApi } from '../../types/podman-desktop-api'; +import type { Webview } from '@podman-desktop/api'; + +export interface IMessage { + id: number; + channel: string; +} + +export interface IMessageRequest extends IMessage{ + args: any[]; +} + +export interface IMessageResponse extends IMessageRequest { + status: 'error' | 'success'; + error?: string | undefined; + body: any; +} + +export function isMessageRequest(content: any): content is IMessageRequest { + return content !== undefined && content !== null && 'id' in content && 'channel' in content; +} + +export function isMessageResponse(content: any): content is IMessageResponse { + return isMessageRequest(content) && 'status' in content +} + +export class RpcExtension { + methods: Map Promise> = new Map(); + + constructor(private webview: Webview) { + this.init(); + } + + init() { + this.webview.onDidReceiveMessage(async (message: any) => { + console.log('RpcExtension onDidReceiveMessage', message); + + if(!isMessageRequest(message)) { + console.error("Received incompatible message.", message); + return; + } + + if(!this.methods.has(message.channel)) { + console.error(`Trying to call on an unknown channel ${message.channel}. Available: ${Array.from(this.methods.keys())}`); + return; + } + + try { + const result = await this.methods.get(message.channel)?.(...message.args); + this.webview.postMessage({ + id: message.id, + channel: message.channel, + body: result, + status: 'success', + } as IMessageResponse); + } catch (e) { + this.webview.postMessage({ + id: message.id, + channel: message.channel, + body: undefined, + error: `Something went wrong on channel ${message.channel}: ${String(e)}` + } as IMessageResponse); + } + }); + } + + registerInstance(classType: { new (): T }, instance: T) { + const methodNames = Object.getOwnPropertyNames(classType.prototype) + .filter(name => name !== 'constructor' && typeof instance[name] === 'function'); + + methodNames.forEach(name => { + const method = instance[name].bind(instance); + this.register(name, method); + }); + } + + register(channel: string, method: (body: any) => any) { + console.log(`RpcExtension register "${channel}"`); + this.methods.set(channel, method); + } +} + +export class RpcBrowser { + counter: number = 0; + promises: Map any, reject: (value: unknown) => void}> = new Map(); + + getUniqueId(): number { + return ++this.counter; + } + + constructor(private window: Window, private api: PodmanDesktopApi) { + this.init(); + } + + init() { + this.window.addEventListener('message', (event: MessageEvent) => { + const message = event.data; + if(!isMessageResponse(message)) { + console.error("Received incompatible message.", message); + return; + } + + if(!this.promises.has(message.id)) { + console.error('Unknown message id.'); + return; + } + + const { resolve, reject } = this.promises.get(message.id) || {}; + + if(message.status === 'error') { + reject?.(message.error) + } else { + resolve?.(message.body); + } + this.promises.delete(message.id); + }) + } + + getProxy(): T { + const thisRef = this; + const proxyHandler: ProxyHandler = { + get(target, prop, receiver) { + if (typeof prop === 'string') { + return (...args: any[]) => { + const channel = prop.toString(); + return thisRef.invoke(channel, ...args); + }; + } + return Reflect.get(target, prop, receiver); + }, + }; + + return new Proxy({}, proxyHandler) as T; + } + + async invoke(channel: string, ...args: any[]): Promise { + // Generate a unique id for the request + const requestId = this.getUniqueId(); + + // Post the message + this.api.postMessage({ + id: requestId, + channel: channel, + args: args + } as IMessageRequest); + + // Add some timeout + setTimeout(() => { + const {reject} = this.promises.get(requestId) || {}; + if(!reject) + return; + reject(new Error('Timeout')); + this.promises.delete(requestId); + }, 10000); + + // Create a Promise + return new Promise((resolve, reject) => { + this.promises.set(requestId, {resolve, reject}); + }) + } +} diff --git a/packages/shared/StudioAPI.ts b/packages/shared/StudioAPI.ts index 9e83a10d5..aa7e698a3 100644 --- a/packages/shared/StudioAPI.ts +++ b/packages/shared/StudioAPI.ts @@ -1,12 +1,12 @@ -import type { Recipe } from './models/IRecipe'; -import type { Category } from './models/ICategory'; +import type { Recipe } from '@shared/models/IRecipe'; +import type { Category } from '@shared/models/ICategory'; -export interface StudioAPI { - ping(): Promise; - getRecentRecipes(): Promise; - getCategories(): Promise; - getRecipesByCategory(categoryId: string): Promise; - getRecipeById(recipeId: string): Promise; - searchRecipes(query: string): Promise; +export abstract class StudioAPI { + abstract ping(): Promise; + abstract getRecentRecipes(): Promise; + abstract getCategories(): Promise; + abstract getRecipesByCategory(categoryId: string): Promise; + abstract getRecipeById(recipeId: string): Promise; + abstract searchRecipes(query: string): Promise; } diff --git a/types/podman-desktop-api.d.ts b/types/podman-desktop-api.d.ts index 4bb9fede2..33276d33a 100644 --- a/types/podman-desktop-api.d.ts +++ b/types/podman-desktop-api.d.ts @@ -1,7 +1,7 @@ // podman-desktop-api.d.ts declare global { - interface PodmanDesktopApi { + export interface PodmanDesktopApi { getState: () => any; postMessage: (msg: any) => void; setState: (newState: any) => void; @@ -10,4 +10,4 @@ declare global { function acquirePodmanDesktopApi(): PodmanDesktopApi; } -export {}; +export { PodmanDesktopApi }; From 68e90bd6c7ed0f36b4778bec2834f3a2eb9363d6 Mon Sep 17 00:00:00 2001 From: axel7083 <42176370+axel7083@users.noreply.github.com> Date: Wed, 10 Jan 2024 15:25:19 -0500 Subject: [PATCH 2/5] fix: removing the ai file from front in favor of back --- packages/frontend/src/utils/ai.json | 35 ----------------------------- 1 file changed, 35 deletions(-) delete mode 100644 packages/frontend/src/utils/ai.json diff --git a/packages/frontend/src/utils/ai.json b/packages/frontend/src/utils/ai.json deleted file mode 100644 index 570219221..000000000 --- a/packages/frontend/src/utils/ai.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "recipes": [ - { - "id": "chatbot", - "description" : "Chat bot application", - "name" : "ChatBot", - "repository": "https://github.com/michaelclifford/locallm", - "categories": [ - "natural-language-processing" - ] - } - ], - "categories": [ - { - "id": "natural-language-processing", - "name": "Natural Language Processing", - "description" : "Models that work with text: classify, summarize, translate, or generate text." - }, - { - "id": "computer-vision", - "description" : "Process images, from classification to object detection and segmentation.", - "name" : "Computer Vision" - }, - { - "id": "audio", - "description" : "Recognize speech or classify audio with audio models.", - "name" : "Audio" - }, - { - "id": "multimodal", - "description" : "Stuff about multimodal models goes here omg yes amazing.", - "name" : "Multimodal" - } - ] -} From bccf85365011c116193b5251740abd85ffd429e9 Mon Sep 17 00:00:00 2001 From: axel7083 <42176370+axel7083@users.noreply.github.com> Date: Wed, 10 Jan 2024 15:39:14 -0500 Subject: [PATCH 3/5] feat: adding getPullingStatus status --- packages/backend/src/studio-api-impl.ts | 5 +++++ packages/shared/StudioAPI.ts | 2 ++ 2 files changed, 7 insertions(+) diff --git a/packages/backend/src/studio-api-impl.ts b/packages/backend/src/studio-api-impl.ts index a03a0d126..4b1ac3f5d 100644 --- a/packages/backend/src/studio-api-impl.ts +++ b/packages/backend/src/studio-api-impl.ts @@ -2,10 +2,15 @@ import type { StudioAPI } from '@shared/StudioAPI'; import { Category } from '@shared/models/ICategory'; import { Recipe } from '@shared/models/IRecipe'; import content from './ai.json'; +import { Task } from '@shared/models/ITask'; export const RECENT_CATEGORY_ID = 'recent-category'; export class StudioApiImpl implements StudioAPI { + async getPullingStatus(recipeId: string): Promise { + return []; + } + async ping(): Promise { return 'pong'; } diff --git a/packages/shared/StudioAPI.ts b/packages/shared/StudioAPI.ts index aa7e698a3..c964030db 100644 --- a/packages/shared/StudioAPI.ts +++ b/packages/shared/StudioAPI.ts @@ -1,5 +1,6 @@ import type { Recipe } from '@shared/models/IRecipe'; import type { Category } from '@shared/models/ICategory'; +import { Task } from '@shared/models/ITask'; export abstract class StudioAPI { abstract ping(): Promise; @@ -8,5 +9,6 @@ export abstract class StudioAPI { abstract getRecipesByCategory(categoryId: string): Promise; abstract getRecipeById(recipeId: string): Promise; abstract searchRecipes(query: string): Promise; + abstract getPullingStatus(recipeId: string): Promise } From e8b7016f37275a0e03540f31b650416fd184d0dd Mon Sep 17 00:00:00 2001 From: axel7083 <42176370+axel7083@users.noreply.github.com> Date: Wed, 10 Jan 2024 15:46:42 -0500 Subject: [PATCH 4/5] feat: adding some methods for lucas --- packages/backend/src/studio-api-impl.ts | 10 ++++++++++ packages/frontend/src/pages/Recipe.svelte | 7 +------ packages/shared/StudioAPI.ts | 1 + 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/packages/backend/src/studio-api-impl.ts b/packages/backend/src/studio-api-impl.ts index 4b1ac3f5d..813e55fc9 100644 --- a/packages/backend/src/studio-api-impl.ts +++ b/packages/backend/src/studio-api-impl.ts @@ -7,6 +7,9 @@ import { Task } from '@shared/models/ITask'; export const RECENT_CATEGORY_ID = 'recent-category'; export class StudioApiImpl implements StudioAPI { + + private status: Map = new Map(); + async getPullingStatus(recipeId: string): Promise { return []; } @@ -38,4 +41,11 @@ export class StudioApiImpl implements StudioAPI { async searchRecipes(query: string): Promise { return []; // todo: not implemented } + + async pullApplication(recipeId: string): Promise { + const recipe: Recipe = await this.getRecipeById(recipeId); + + //todo: stuff here + return Promise.resolve(undefined); + } } diff --git a/packages/frontend/src/pages/Recipe.svelte b/packages/frontend/src/pages/Recipe.svelte index 69fe230c4..29e8b78f9 100644 --- a/packages/frontend/src/pages/Recipe.svelte +++ b/packages/frontend/src/pages/Recipe.svelte @@ -28,12 +28,7 @@ onMount(async () => { }) const onPullingRequest = () => { - pulling = [ - {state: 'success', name: 'Pulling image:latest'}, - {state: 'error', name: 'Pulling database:latest'}, - {state: 'loading', name: 'Pulling redis:latest'}, - {state: 'loading', name: 'Downloading model:latest'}, - ] + studioClient.pullApplication(recipeId); } diff --git a/packages/shared/StudioAPI.ts b/packages/shared/StudioAPI.ts index c964030db..42e96916e 100644 --- a/packages/shared/StudioAPI.ts +++ b/packages/shared/StudioAPI.ts @@ -10,5 +10,6 @@ export abstract class StudioAPI { abstract getRecipeById(recipeId: string): Promise; abstract searchRecipes(query: string): Promise; abstract getPullingStatus(recipeId: string): Promise + abstract pullApplication(recipeId: string): Promise; } From 1e276d276640f5b75e49641bbdf6caec0c40d8db Mon Sep 17 00:00:00 2001 From: axel7083 <42176370+axel7083@users.noreply.github.com> Date: Wed, 10 Jan 2024 15:51:37 -0500 Subject: [PATCH 5/5] fix: typo --- packages/frontend/src/App.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/frontend/src/App.svelte b/packages/frontend/src/App.svelte index 639f00e5a..94c644d3c 100644 --- a/packages/frontend/src/App.svelte +++ b/packages/frontend/src/App.svelte @@ -1,5 +1,5 @@