-
Notifications
You must be signed in to change notification settings - Fork 11
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Add aiElement Filter (#CO-731)
feat: Add AIElement Filter (#CO-731)
- Loading branch information
Showing
9 changed files
with
524 additions
and
60 deletions.
There are no files selected for viewing
92 changes: 92 additions & 0 deletions
92
packages/askui-nodejs/src/core/ai-element/ai-element-collection.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
import os from 'os'; | ||
import path from 'path'; | ||
import fs from 'fs-extra'; | ||
import { AIElement, AIElementJson } from './ai-element'; | ||
import { CustomElementJson } from '../model/custom-element-json'; | ||
import { logger } from '../../lib'; | ||
import { AIElementError } from './ai-element-error'; | ||
|
||
export class AIElementCollection { | ||
static AI_ELEMENT_FOLDER = path.join( | ||
os.homedir(), | ||
'.askui', | ||
'SnippingTool', | ||
'AIElement', | ||
); | ||
|
||
constructor(private elements: AIElement[]) {} | ||
|
||
static async collectForWorkspaceId( | ||
workspaceId: string | undefined, | ||
): Promise<AIElementCollection> { | ||
logger.debug(`Collecting AIElements for workspace '${workspaceId}' ...`); | ||
|
||
if (workspaceId === undefined) { | ||
throw new AIElementError("Value of 'workspaceId' must be defined."); | ||
} | ||
|
||
const workspaceAIElementFolder = path.join( | ||
AIElementCollection.AI_ELEMENT_FOLDER, | ||
workspaceId, | ||
); | ||
|
||
if (!(await fs.pathExists(workspaceAIElementFolder))) { | ||
throw new AIElementError( | ||
`Missing AIElement folder for workspace '${workspaceId}' at '${workspaceAIElementFolder}'.`, | ||
); | ||
} | ||
|
||
const files = await fs.readdir(workspaceAIElementFolder); | ||
|
||
if (files.length === 0) { | ||
throw new AIElementError( | ||
`'${workspaceAIElementFolder}' is empty. No AIElement files found for workspace '${workspaceId}'.`, | ||
); | ||
} | ||
|
||
const aiElements = await Promise.all(files | ||
.filter((file) => path.extname(file) === '.json') | ||
.map(async (file) => { | ||
const jsonFile = path.join(workspaceAIElementFolder, file); | ||
const baseName = path.basename(jsonFile, '.json'); | ||
const pngFile = path.join(workspaceAIElementFolder, `${baseName}.png`); | ||
if (await fs.pathExists(pngFile)) { | ||
const metadata: AIElementJson = JSON.parse(await fs.readFile(jsonFile, 'utf-8')); | ||
return AIElement.fromJson(metadata, pngFile); | ||
} | ||
return null; | ||
})); | ||
|
||
const validAIElements = aiElements.filter((element): element is AIElement => element !== null); | ||
|
||
if (validAIElements.length === 0) { | ||
throw new AIElementError( | ||
`No AIElement files found for workspace '${workspaceId}' at '${workspaceAIElementFolder}'.`, | ||
); | ||
} | ||
|
||
return new AIElementCollection(validAIElements); | ||
} | ||
|
||
getByName(name: string): CustomElementJson[] { | ||
if (name === '') { | ||
throw new AIElementError("Parameter 'name' must be non-empty. This might be due to corrupted metadata."); | ||
} | ||
|
||
logger.debug(`Getting all CustomElementJson with the name '${name}' ...`); | ||
|
||
const elements = this.elements.filter((element) => element.hasName(name)); | ||
if (elements.length === 0) { | ||
throw new AIElementError(`No AIElement with the name '${name}' was found.`); | ||
} | ||
|
||
return elements.map((element) => element.toCustomElement()); | ||
} | ||
|
||
getByNames(names: string[]): CustomElementJson[] { | ||
if (names.length === 0) { | ||
return []; | ||
} | ||
return names.flatMap((name) => this.getByName(name)); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export class AIElementError extends Error { } |
69 changes: 69 additions & 0 deletions
69
packages/askui-nodejs/src/core/ai-element/ai-element.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
import { AIElement, AIElementJson } from './ai-element'; | ||
import { CustomElementJson } from '../model/custom-element-json'; | ||
|
||
describe('AIElement', () => { | ||
const dummyAIElementMetadata: AIElementJson = { | ||
image: { | ||
mask: [ | ||
{ x: 1, y: 1 }, | ||
{ x: 2, y: 2 }, | ||
], | ||
}, | ||
name: 'test-element', | ||
version: 1, | ||
}; | ||
const dummyImagePath = '/path/to/image.png'; | ||
|
||
it('should create an AIElement from a valid JSON', () => { | ||
const element = AIElement.fromJson(dummyAIElementMetadata, dummyImagePath); | ||
expect(element).toBeInstanceOf(AIElement); | ||
expect(element.name).toBe(dummyAIElementMetadata.name); | ||
expect(element.imagePath).toBe(dummyImagePath); | ||
expect(element.mask).toEqual(dummyAIElementMetadata.image?.mask); | ||
}); | ||
|
||
it('should throw an error for unsupported version', () => { | ||
const invalidMetadata = { ...dummyAIElementMetadata, version: 2 }; | ||
expect(() => AIElement.fromJson(invalidMetadata, dummyImagePath)).toThrow( | ||
'Unsupported AIElement version', | ||
); | ||
}); | ||
|
||
it('should convert AIElement to CustomElementJson', () => { | ||
const element = new AIElement( | ||
dummyAIElementMetadata.name, | ||
dummyImagePath, | ||
dummyAIElementMetadata.image?.mask, | ||
); | ||
const customElement = element.toCustomElement(); | ||
expect(customElement).toEqual<CustomElementJson>({ | ||
customImage: dummyImagePath, | ||
mask: dummyAIElementMetadata.image?.mask, | ||
name: dummyAIElementMetadata.name, | ||
}); | ||
}); | ||
|
||
it('should convert AIElement without a mask to CustomElementJson without a mask', () => { | ||
const element = new AIElement( | ||
dummyAIElementMetadata.name, | ||
dummyImagePath, | ||
undefined, | ||
); | ||
const customElement = element.toCustomElement(); | ||
expect(customElement).toEqual<CustomElementJson>({ | ||
customImage: dummyImagePath, | ||
mask: undefined, | ||
name: dummyAIElementMetadata.name, | ||
}); | ||
}); | ||
|
||
it('should check if element has a specific name', () => { | ||
const element = new AIElement( | ||
dummyAIElementMetadata.name, | ||
dummyImagePath, | ||
dummyAIElementMetadata.image?.mask, | ||
); | ||
expect(element.hasName(dummyAIElementMetadata.name)).toBe(true); | ||
expect(element.hasName('other-name')).toBe(false); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
import { CustomElementJson } from '../model/custom-element-json'; | ||
import { logger } from '../../lib'; | ||
import { AIElementError } from './ai-element-error'; | ||
|
||
interface AIElementJson { | ||
version: number; | ||
name: string; | ||
image?: { mask?: { x: number; y: number }[] }; | ||
} | ||
|
||
class AIElement { | ||
constructor( | ||
public name: string, | ||
public imagePath: string, | ||
public mask?: { x: number; y: number }[], | ||
) {} | ||
|
||
static fromJson(json: AIElementJson, imagePath: string): AIElement { | ||
if (json.version === 1) { | ||
return new AIElement(json.name, imagePath, json.image?.mask); | ||
} | ||
throw new AIElementError(`Unsupported AIElement version '${json.version}'.`); | ||
} | ||
|
||
toCustomElement(): CustomElementJson { | ||
logger.debug('Converting AIElement to CustomElementJson.'); | ||
return { | ||
customImage: this.imagePath, | ||
mask: this.mask, | ||
name: this.name, | ||
}; | ||
} | ||
|
||
hasName(name: string): boolean { | ||
return this.name === name; | ||
} | ||
} | ||
|
||
export { AIElement, AIElementJson }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.