diff --git a/src/model/.kbot/params.json b/src/model/.kbot/params.json
index 4e39d2f..8cd6687 100644
--- a/src/model/.kbot/params.json
+++ b/src/model/.kbot/params.json
@@ -1,5 +1,5 @@
{
- "model": "anthropic/claude-3.7-sonnet",
+ "model": "anthropic/claude-3.7-sonnet:thinking",
"messages": [
{
"role": "user",
@@ -7,121 +7,18 @@
},
{
"role": "user",
- "content": "USER Preferences : ## Todos\r\n\r\n- for Astro, tailwind\r\n- no react or additional dependencies\r\n- IHowto: import { IHowto } from \"@/model/howto.js\"\r\n- For text, use {text} (import { i18n as Translate } from \"@polymech/astro-base\")\r\n- data is provided via `import { getCollection } from 'astro:content'\r\nconst items = await getCollection('howtos')`\r\n\r\n### Example Tailwind Sidebar\r\n\r\n\r\n \r\n \r\n\r\n## Todos\r\n\r\n- [ ] create minimal Astro component, a sidebar using flowbit (see example) for the given interface (\"IHowto\") and sample data ../components/libary/howto.astro, use the 'category' field, save as ../components/howtos/sidebar2.astro\r\n"
+ "content": "USER Preferences : ## Todos\r\n\r\n- we use Typescript, ESM, minimal dependencies, functional programming\r\n- dont document or comment, return just the code, ready to go\r\n\r\n## Todos\r\n\r\n- [ ] function, to convert a howto (howto-model.ts) to json-ld structured data, satisfying Google, \"toJSON-LD\"\r\n"
},
{
"role": "user",
- "path": "howto.ts",
- "content": "import * as path from 'path'\r\nimport { findUp } from 'find-up'\r\nimport { sync as read } from '@polymech/fs/read'\r\nimport { sync as exists } from '@polymech/fs/exists'\r\nimport { filesEx, forward_slash, resolveConfig, resolve } from '@polymech/commons'\r\nimport { ICADNodeSchema, IComponentConfig } from '@polymech/commons/component'\r\nimport { RenderedContent, DataEntry } from \"astro:content\"\r\nimport type { Loader, LoaderContext } from 'astro/loaders'\r\nimport { get } from '@polymech/commons/component'\r\n\r\nimport {\r\n CAD_MAIN_MATCH, PRODUCT_BRANCHES,\r\n CAD_EXTENSIONS, CAD_MODEL_EXT, PRODUCT_DIR, PRODUCT_GLOB,\r\n PRODUCT_ROOT, CAD_EXPORT_CONFIGURATIONS,\r\n CAD_DEFAULT_CONFIGURATION,\r\n CAD_URL,\r\n parseBoolean,\r\n DEFAULT_CONTACT,\r\n default_image,\r\n HOWTO_ROOT,\r\n HOWTO_GLOB\r\n} from 'config/config.js'\r\n\r\nimport { env } from '@/base/index.js'\r\nimport { gallery } from '@/base/media.js';\r\n\r\nimport { PFilterValid } from '@polymech/commons/filter'\r\n\r\nimport { IAssemblyData } from '@polymech/cad'\r\nimport { logger as log } from '@/base/index.js'\r\nimport { translate } from \"@/base/i18n.js\"\r\nimport { I18N_SOURCE_LANGUAGE } from \"config/config.js\"\r\nimport { slugify } from \"@/base/strings.js\"\r\nimport { HOWTO_MIGRATION } from '@/app/config.js'\r\n\r\nexport const ITEM_TYPE = 'howto'\r\n\r\n//export const load = () => get(`${HOWTO_ROOT()}/${HOWTO_GLOB}`, HOWTO_ROOT(), ITEM_TYPE)\r\n\r\nexport const howtos = async () => {\r\n const src = HOWTO_MIGRATION()\r\n //const kb_out = path.resolve(substitute(\"${KB_ROOT}\", DEFAULT_ROOTS));\r\n const data = read(src, 'json') as any;\r\n let howtos = data.v3_howtos as any[]\r\n howtos = howtos.filter((h) => h.moderation == 'accepted');\r\n const tags = data.v3_tags;\r\n\r\n let output = {\r\n tags: {},\r\n howtows: {}\r\n }\r\n howtos.forEach((howto) => {\r\n const howtoTags: any = [];\r\n for (const ht in howto.tags) {\r\n const gt: any = tags.find((t) => t._id === ht) || { label: 'untagged' };\r\n if (gt) {\r\n howtoTags.push(gt.label || \"\");\r\n if (!output.tags[gt.label]) {\r\n output.tags[gt.label] = []\r\n }\r\n output.tags[gt.label].push(howto.slug.trim());\r\n }\r\n }\r\n\r\n howto.user = data.v3_mappins.find((u) => u._id == howto._createdBy);\r\n howto.tags = howtoTags;\r\n /*\r\n howto.image = `/howtos/${howto.slug}/${encodeURIComponent(sanitize(howto.cover_image.name))}`,\r\n howto.cover_image.name = sanitize(howto.cover_image.name);\r\n */\r\n //output.howtows[howto.slug.trim()] = howto;\r\n\r\n });\r\n return howtos\r\n}\r\n\r\nexport const defaults = async (data: any, cwd: string, root: string) => {\r\n let defaultsJSON = await findUp('defaults.json', {\r\n stopAt: root,\r\n cwd: cwd\r\n });\r\n try {\r\n if (defaultsJSON) {\r\n data = {\r\n ...read(defaultsJSON, 'json') as any,\r\n ...data,\r\n };\r\n }\r\n } catch (error) {\r\n\r\n }\r\n return data;\r\n};\r\n\r\n\r\nconst onItem = async (item: any, ctx: LoaderContext) => {\r\n return item\r\n\r\n if (!item || !item.data) {\r\n ctx.logger.error(`Error completing ${''}: no data`);\r\n return\r\n }\r\n let data: any = item.data\r\n const itemRel = data.rel\r\n const itemRelMin = itemRel.replace('products/', '')\r\n const itemDir = PRODUCT_DIR(itemRel)\r\n const default_profile = env(itemRel)\r\n data.product_rel = itemRelMin\r\n data.assets = {\r\n renderings: [],\r\n gallery: []\r\n }\r\n data.body = (read(path.join(itemDir, 'templates/shared', 'body.md')) as string) || \"\"\r\n data.resources = (read(path.join(itemDir, 'templates/shared', 'resources.md')) as string) || \"\"\r\n //////////////////////////////////////////\r\n //\r\n // Item Shared Resources\r\n //\r\n let resourcesDefaultPath = await findUp('resources.md', {\r\n stopAt: PRODUCT_ROOT(),\r\n cwd: itemDir\r\n }) || \"\"\r\n data.shared = (read(resourcesDefaultPath) as string) || \"\"\r\n //////////////////////////////////////////\r\n //\r\n // Readme\r\n //\r\n let readmePath = path.join(itemDir, 'Readme.md')\r\n data.readme = (read(readmePath) as string) || \"\"\r\n //////////////////////////////////////////\r\n //\r\n // Variables\r\n //\r\n data = await defaults(data, itemDir, PRODUCT_ROOT());\r\n data = {\r\n ...data,\r\n ...default_profile.variables,\r\n product_rel_min: itemRelMin,\r\n }\r\n // data = resolveConfig(data as Record) as IComponentConfigEx\r\n item.data = data\r\n //////////////////////////////////////////\r\n //\r\n // Extensions, CAD, Media, etcetera etcetera :)\r\n //\r\n //data.cad = await cad(item)\r\n data.assets.gallery = await gallery('media/gallery', data.rel)\r\n}\r\n\r\nexport function loader(): Loader {\r\n\r\n const load = async ({\r\n config,\r\n logger,\r\n watcher,\r\n parseData,\r\n store,\r\n generateDigest }: LoaderContext) => {\r\n\r\n store.clear()\r\n let items = await howtos()\r\n for (const item of items) {\r\n const id = item.slug\r\n const data = {\r\n slug: item.slug,\r\n id,\r\n title: item.title,\r\n type: ITEM_TYPE,\r\n components: [],\r\n item\r\n }\r\n //const parsedData = await parseData({ id, data: data }); \r\n const storeItem = {\r\n digest: await generateDigest(data),\r\n filePath: id,\r\n id: `${item.slug}`,\r\n data: data\r\n }\r\n await onItem(storeItem, {\r\n logger,\r\n watcher,\r\n parseData,\r\n store,\r\n generateDigest\r\n } as any)\r\n\r\n storeItem.data['config'] = JSON.stringify(storeItem.data, null, 2)\r\n store.set(storeItem)\r\n }\r\n }\r\n return {\r\n name: `astro:store:${ITEM_TYPE}`,\r\n load\r\n };\r\n}\r\n\r\n\r\nexport interface IHowto {\r\n _createdBy: string\r\n mentions: any[]\r\n _deleted: boolean\r\n fileLink: string\r\n slug: string\r\n _modified: string\r\n previousSlugs: string[]\r\n _created: string\r\n description: string\r\n votedUsefulBy: string[]\r\n creatorCountry: string\r\n total_downloads: number\r\n title: string\r\n time: string\r\n files: any[]\r\n difficulty_level: string\r\n _id: string\r\n tags?: Tags\r\n total_views: number\r\n _contentModifiedTimestamp: string\r\n cover_image: CoverImage\r\n comments: any[]\r\n moderatorFeedback: string\r\n steps: Step[]\r\n moderation: string\r\n}\r\n\r\nexport interface Tags {\r\n [key: string]: boolean\r\n}\r\n\r\nexport interface CoverImage {\r\n name: string\r\n downloadUrl: string\r\n type: string\r\n fullPath: string\r\n updated: string\r\n size: number\r\n timeCreated: string\r\n contentType: string\r\n}\r\n\r\nexport interface Step {\r\n title: string\r\n text: string\r\n images: Image[]\r\n _animationKey: string\r\n}\r\nexport interface Image {\r\n updated: string\r\n size: number\r\n fullPath: string\r\n timeCreated: string\r\n name: string\r\n downloadUrl: string\r\n contentType: string\r\n type: string\r\n}\r\n\r\n"
+ "path": "howto-model.ts",
+ "content": "export const ITEM_TYPE = 'howto'\r\n////////////////////////////////\r\n//\r\n// Interfaces - Old\r\nexport interface IHowto {\r\n _createdBy: string\r\n mentions: any[]\r\n _deleted: boolean\r\n fileLink: string\r\n slug: string\r\n _modified: string\r\n previousSlugs: string[]\r\n _created: string\r\n description: string\r\n votedUsefulBy: string[]\r\n creatorCountry: string\r\n total_downloads: number\r\n title: string\r\n time: string\r\n files: any[]\r\n category: IOACategory\r\n difficulty_level: string\r\n _id: string\r\n tags?: IOATag[]\r\n total_views: number\r\n _contentModifiedTimestamp: string\r\n cover_image: Image\r\n comments: any[]\r\n moderatorFeedback: string\r\n steps: Step[]\r\n moderation: string\r\n user?: User\r\n}\r\n\r\nexport interface Tags {\r\n [key: string]: boolean\r\n}\r\n\r\nexport interface CoverImage {\r\n name: string\r\n downloadUrl: string\r\n type: string\r\n fullPath: string\r\n updated: string\r\n size: number\r\n timeCreated: string\r\n contentType: string\r\n}\r\n\r\nexport interface Step {\r\n title: string\r\n text: string\r\n images: Image[]\r\n _animationKey: string\r\n}\r\nexport interface Image {\r\n updated: string\r\n size: number\r\n fullPath: string\r\n timeCreated: string\r\n name: string\r\n downloadUrl: string\r\n contentType: string\r\n type: string\r\n src: string,\r\n alt: string\r\n}\r\n\r\n/// Taxonomy\r\nexport interface IOACategory {\r\n _created: string\r\n _id: string\r\n _deleted: boolean\r\n label: string\r\n _modified: string\r\n}\r\n\r\nexport interface IOATag {\r\n categories: string[]\r\n image: string\r\n _created: string\r\n _deleted: boolean\r\n label: string\r\n _createdBy: string\r\n _modified: string\r\n _id: string\r\n}\r\n\r\nexport interface User {\r\n _modified: string\r\n _id: string\r\n subType: string\r\n moderation: string\r\n _deleted: boolean\r\n verified: boolean\r\n type: string\r\n location: Location\r\n _created: string\r\n geo: Geo\r\n data: Data\r\n detail: Detail\r\n}\r\n\r\nexport interface Location {\r\n lat: number\r\n lng: number\r\n}\r\n\r\nexport interface Geo {\r\n latitude: number\r\n lookupSource: string\r\n longitude: number\r\n localityLanguageRequested: string\r\n continent: string\r\n continentCode: string\r\n countryName: string\r\n countryCode: string\r\n principalSubdivision: string\r\n principalSubdivisionCode: string\r\n city: string\r\n locality: string\r\n postcode: string\r\n plusCode: string\r\n localityInfo: LocalityInfo\r\n}\r\n\r\nexport interface LocalityInfo {\r\n administrative: Administrative[]\r\n informative: Informative[]\r\n}\r\n\r\nexport interface Administrative {\r\n name: string\r\n description: string\r\n isoName?: string\r\n order: number\r\n adminLevel: number\r\n isoCode?: string\r\n wikidataId: string\r\n geonameId: number\r\n}\r\n\r\nexport interface Informative {\r\n name: string\r\n description: string\r\n isoName?: string\r\n order: number\r\n isoCode?: string\r\n wikidataId?: string\r\n geonameId?: number\r\n}\r\n\r\nexport interface Data {\r\n urls: Url[]\r\n description: string\r\n services: Service[]\r\n title: string\r\n images: any[]\r\n}\r\n\r\nexport interface Url {\r\n name: string\r\n url: string\r\n}\r\n\r\nexport interface Service {\r\n welding: boolean\r\n assembling: boolean\r\n machining: boolean\r\n electronics: boolean\r\n molds: boolean\r\n}\r\n\r\nexport interface Detail {\r\n services: any[]\r\n urls: any[]\r\n}\r\n"
},
{
"role": "user",
- "path": "howto.json",
- "content": "{\r\n \"_createdBy\": \"gus-merckel\",\r\n \"mentions\": [],\r\n \"_deleted\": false,\r\n \"fileLink\": \"\",\r\n \"slug\": \"cut-out-shapes-out-of-plastic-sheets-with-a-cnc-\",\r\n \"_modified\": \"2023-10-27T18:09:36.519Z\",\r\n \"previousSlugs\": [\r\n \"cut-out-shapes-out-of-plastic-sheets-with-a-cnc-\"\r\n ],\r\n \"_created\": \"2023-08-23T18:20:09.098Z\",\r\n \"description\": \"In this how to, I will show you our process to cut HDPE Sheets using a X-Carve CNC.\\n\\nHere is the full video in spanish with subtitles https://www.youtube.com/watch?v=4LrrFz802To \",\r\n \"votedUsefulBy\": [\r\n \"sigolene\",\r\n \"mattia\",\r\n \"uillinoispreciousplastics\"\r\n ],\r\n \"creatorCountry\": \"mx\",\r\n \"total_downloads\": 0,\r\n \"title\": \"Cut out shapes out of plastic sheets with a CNC \",\r\n \"time\": \"< 5 hours\",\r\n \"files\": [],\r\n \"difficulty_level\": \"Medium\",\r\n \"_id\": \"038gjWgLjiyYknbEjDeI\",\r\n \"tags\": {\r\n \"RTCBJAFa05YBVVBy0KeO\": true\r\n },\r\n \"category\":\"machines\",\r\n \"total_views\": 232,\r\n \"_contentModifiedTimestamp\": \"2023-08-23T18:20:09.098Z\",\r\n \"cover_image\": {\r\n \"name\": \"IMG_20200605_142311.jpg\",\r\n \"downloadUrl\": \"https://firebasestorage.googleapis.com/v0/b/onearmyworld.appspot.com/o/uploads%2Fhowtos%2F038gjWgLjiyYknbEjDeI%2FIMG_20200605_142311.jpg?alt=media&token=c272c174-1adc-45af-967b-771adce7295d\",\r\n \"type\": \"image/jpeg\",\r\n \"fullPath\": \"uploads/howtos/038gjWgLjiyYknbEjDeI/IMG_20200605_142311.jpg\",\r\n \"updated\": \"2021-04-05T15:09:00.605Z\",\r\n \"size\": 124661,\r\n \"timeCreated\": \"2021-04-05T15:09:00.605Z\",\r\n \"contentType\": \"image/jpeg\"\r\n },\r\n \"comments\": [],\r\n \"moderatorFeedback\": \"\",\r\n \"steps\": [\r\n {\r\n \"title\": \"Measure the plastic sheet\",\r\n \"text\": \"For this step we need to measure our plastic sheet: Height, Width and Thickness. Our X-Carve machine works with the CAM Software EASEL, for me, the easiest software for CNC milling out there. \\n\\nThe cool thing about Easel (https://easel.inventables.com/) is that you can \\\"simulate\\\" your actual material and THEY EVEN HAVE HDPE 2-Colors in their cutting material lists!!\\n\\n\\n\",\r\n \"images\": [\r\n {\r\n \"fullPath\": \"uploads/howtos/pbo0Pe44aTngvlD04kGf/1.jpg\",\r\n \"name\": \"1.jpg\",\r\n \"size\": 74095,\r\n \"type\": \"image/jpeg\",\r\n \"timeCreated\": \"2021-03-26T19:42:05.766Z\",\r\n \"contentType\": \"image/jpeg\",\r\n \"downloadUrl\": \"https://firebasestorage.googleapis.com/v0/b/onearmyworld.appspot.com/o/uploads%2Fhowtos%2Fpbo0Pe44aTngvlD04kGf%2F1.jpg?alt=media&token=293d733d-05a5-494a-9340-47f4564f1939\",\r\n \"updated\": \"2021-03-26T19:42:05.766Z\"\r\n },\r\n {\r\n \"contentType\": \"image/jpeg\",\r\n \"timeCreated\": \"2021-03-26T19:42:05.669Z\",\r\n \"updated\": \"2021-03-26T19:42:05.669Z\",\r\n \"size\": 69665,\r\n \"downloadUrl\": \"https://firebasestorage.googleapis.com/v0/b/onearmyworld.appspot.com/o/uploads%2Fhowtos%2Fpbo0Pe44aTngvlD04kGf%2F2.jpg?alt=media&token=004f50f1-97ac-4df4-9ba9-f463aa4cbca3\",\r\n \"fullPath\": \"uploads/howtos/pbo0Pe44aTngvlD04kGf/2.jpg\",\r\n \"name\": \"2.jpg\",\r\n \"type\": \"image/jpeg\"\r\n }\r\n ],\r\n \"_animationKey\": \"unique1\"\r\n },\r\n {\r\n \"text\": \"Using the CNC clamps from the X-Carve, secure the sheet to the table, \",\r\n \"_animationKey\": \"unique2\",\r\n \"images\": [\r\n {\r\n \"updated\": \"2021-03-26T19:42:06.249Z\",\r\n \"size\": 55544,\r\n \"fullPath\": \"uploads/howtos/pbo0Pe44aTngvlD04kGf/3.jpg\",\r\n \"timeCreated\": \"2021-03-26T19:42:06.249Z\",\r\n \"name\": \"3.jpg\",\r\n \"downloadUrl\": \"https://firebasestorage.googleapis.com/v0/b/onearmyworld.appspot.com/o/uploads%2Fhowtos%2Fpbo0Pe44aTngvlD04kGf%2F3.jpg?alt=media&token=0b9c1914-1c75-429e-b34a-1e2b3706edef\",\r\n \"contentType\": \"image/jpeg\",\r\n \"type\": \"image/jpeg\"\r\n }\r\n ],\r\n \"title\": \"Secure sheet \"\r\n },\r\n {\r\n \"title\": \"Choosing a file to cut \",\r\n \"text\": \"Now we go to our illustrator, such as Inkscape to design a vector file or download and open source one frome https://thenounproject.com/.\\n\\nWe download the SVG file, which is an open source vector format and import it to Easel. \\n\",\r\n \"images\": [\r\n {\r\n \"downloadUrl\": \"https://firebasestorage.googleapis.com/v0/b/onearmyworld.appspot.com/o/uploads%2Fhowtos%2Fpbo0Pe44aTngvlD04kGf%2F4.jpg?alt=media&token=1cd2d49d-9335-4bb1-ac2a-e625322ca604\",\r\n \"contentType\": \"image/jpeg\",\r\n \"timeCreated\": \"2021-03-26T19:42:06.727Z\",\r\n \"updated\": \"2021-03-26T19:42:06.727Z\",\r\n \"name\": \"4.jpg\",\r\n \"size\": 42952,\r\n \"type\": \"image/jpeg\",\r\n \"fullPath\": \"uploads/howtos/pbo0Pe44aTngvlD04kGf/4.jpg\"\r\n },\r\n {\r\n \"size\": 69255,\r\n \"fullPath\": \"uploads/howtos/pbo0Pe44aTngvlD04kGf/5.jpg\",\r\n \"updated\": \"2021-03-26T19:42:06.833Z\",\r\n \"timeCreated\": \"2021-03-26T19:42:06.833Z\",\r\n \"downloadUrl\": \"https://firebasestorage.googleapis.com/v0/b/onearmyworld.appspot.com/o/uploads%2Fhowtos%2Fpbo0Pe44aTngvlD04kGf%2F5.jpg?alt=media&token=7cca786a-7d47-43bb-900b-b8d101c276b4\",\r\n \"name\": \"5.jpg\",\r\n \"contentType\": \"image/jpeg\",\r\n \"type\": \"image/jpeg\"\r\n }\r\n ],\r\n \"_animationKey\": \"unique3\"\r\n },\r\n {\r\n \"text\": \"Now with the file we can choose the width we want to carve/cut and then we go to cut and start the wizzard:\\n- We check that the sheet is fixed.\\n- We also specify the cutting bit, we are using a 1/8 flat flute bit. \\n- We tell the machine where the coordinate 0-0 is, which we always choose as the down left corner.\\n- We raise the bit, turn on the Router!!!\\n\\nAND PUM THE MAGIC BEGINS!!\",\r\n \"title\": \"Follow the cutting Wizzard\",\r\n \"images\": [\r\n {\r\n \"timeCreated\": \"2021-03-26T19:42:07.493Z\",\r\n \"size\": 72226,\r\n \"fullPath\": \"uploads/howtos/pbo0Pe44aTngvlD04kGf/6.jpg\",\r\n \"updated\": \"2021-03-26T19:42:07.493Z\",\r\n \"downloadUrl\": \"https://firebasestorage.googleapis.com/v0/b/onearmyworld.appspot.com/o/uploads%2Fhowtos%2Fpbo0Pe44aTngvlD04kGf%2F6.jpg?alt=media&token=ba7195dd-7771-435f-a188-057457697332\",\r\n \"contentType\": \"image/jpeg\",\r\n \"type\": \"image/jpeg\",\r\n \"name\": \"6.jpg\"\r\n },\r\n {\r\n \"fullPath\": \"uploads/howtos/pbo0Pe44aTngvlD04kGf/7.jpg\",\r\n \"size\": 52424,\r\n \"downloadUrl\": \"https://firebasestorage.googleapis.com/v0/b/onearmyworld.appspot.com/o/uploads%2Fhowtos%2Fpbo0Pe44aTngvlD04kGf%2F7.jpg?alt=media&token=a3d5820c-cfe2-484e-8f76-f861ab8b756d\",\r\n \"contentType\": \"image/jpeg\",\r\n \"type\": \"image/jpeg\",\r\n \"timeCreated\": \"2021-03-26T19:42:07.308Z\",\r\n \"updated\": \"2021-03-26T19:42:07.308Z\",\r\n \"name\": \"7.jpg\"\r\n },\r\n {\r\n \"fullPath\": \"uploads/howtos/pbo0Pe44aTngvlD04kGf/8.jpg\",\r\n \"name\": \"8.jpg\",\r\n \"type\": \"image/jpeg\",\r\n \"timeCreated\": \"2021-03-26T19:42:07.346Z\",\r\n \"size\": 55264,\r\n \"contentType\": \"image/jpeg\",\r\n \"downloadUrl\": \"https://firebasestorage.googleapis.com/v0/b/onearmyworld.appspot.com/o/uploads%2Fhowtos%2Fpbo0Pe44aTngvlD04kGf%2F8.jpg?alt=media&token=1c9816d7-3a99-4f41-8d3c-acc2670240f6\",\r\n \"updated\": \"2021-03-26T19:42:07.346Z\"\r\n }\r\n ],\r\n \"_animationKey\": \"uniquenisc2v\"\r\n },\r\n {\r\n \"text\": \"You take now your glasses or object and postprocess them and of course show it to your friends, family and so on.\\n\\n\\n\",\r\n \"images\": [\r\n {\r\n \"fullPath\": \"uploads/howtos/pbo0Pe44aTngvlD04kGf/9.jpg\",\r\n \"contentType\": \"image/jpeg\",\r\n \"timeCreated\": \"2021-03-26T19:42:08.147Z\",\r\n \"downloadUrl\": \"https://firebasestorage.googleapis.com/v0/b/onearmyworld.appspot.com/o/uploads%2Fhowtos%2Fpbo0Pe44aTngvlD04kGf%2F9.jpg?alt=media&token=4dcfe37d-e1ad-41e5-a590-40b4c37c5e1a\",\r\n \"name\": \"9.jpg\",\r\n \"updated\": \"2021-03-26T19:42:08.147Z\",\r\n \"type\": \"image/jpeg\",\r\n \"size\": 82214\r\n }\r\n ],\r\n \"_animationKey\": \"uniquesgl34\",\r\n \"title\": \"Post-production and show case\"\r\n },\r\n {\r\n \"_animationKey\": \"uniquem4y0yi\",\r\n \"title\": \"Hack it and try it yourself\",\r\n \"text\": \"You can try this project with other types of CNC machines, even manual Routers or manual saw, as I did on this video: https://youtu.be/gxkcffQD3eQ, but the important thing is that you share what you do and help this community to grow!!!\\n\\nShare your ideas and comments!\",\r\n \"images\": [\r\n {\r\n \"contentType\": \"image/jpeg\",\r\n \"timeCreated\": \"2021-04-05T15:09:01.445Z\",\r\n \"fullPath\": \"uploads/howtos/038gjWgLjiyYknbEjDeI/IMG_20200605_142311.jpg\",\r\n \"type\": \"image/jpeg\",\r\n \"downloadUrl\": \"https://firebasestorage.googleapis.com/v0/b/onearmyworld.appspot.com/o/uploads%2Fhowtos%2F038gjWgLjiyYknbEjDeI%2FIMG_20200605_142311.jpg?alt=media&token=f94152ff-f923-4054-a3ad-d8ec588856fa\",\r\n \"size\": 124661,\r\n \"updated\": \"2021-04-05T15:09:01.445Z\",\r\n \"name\": \"IMG_20200605_142311.jpg\"\r\n }\r\n ]\r\n }\r\n ],\r\n \"moderation\": \"accepted\"\r\n }"
+ "path": "howto_sample.json",
+ "content": "{\n \"_createdBy\": \"gus-merckel\",\n \"mentions\": [],\n \"_deleted\": false,\n \"fileLink\": \"\",\n \"slug\": \"cut-out-shapes-out-of-plastic-sheets-with-a-cnc-\",\n \"_modified\": \"2023-10-27T18:09:36.519Z\",\n \"previousSlugs\": [\n \"cut-out-shapes-out-of-plastic-sheets-with-a-cnc-\"\n ],\n \"_created\": \"2023-08-23T18:20:09.098Z\",\n \"description\": \"In this how to, I will show you our process to cut HDPE Sheets using a X-Carve CNC.\\n\\nHere is the full video in spanish with subtitles https://www.youtube.com/watch?v=4LrrFz802To \",\n \"votedUsefulBy\": [\n \"sigolene\",\n \"mattia\",\n \"uillinoispreciousplastics\"\n ],\n \"creatorCountry\": \"mx\",\n \"total_downloads\": 0,\n \"title\": \"Cut out shapes out of plastic sheets with a CNC \",\n \"time\": \"< 5 hours\",\n \"files\": [],\n \"difficulty_level\": \"Medium\",\n \"_id\": \"038gjWgLjiyYknbEjDeI\",\n \"tags\": [\n \"HDPE\"\n ],\n \"total_views\": 232,\n \"_contentModifiedTimestamp\": \"2023-08-23T18:20:09.098Z\",\n \"cover_image\": {\n \"name\": \"IMG_20200605_142311.jpg\",\n \"downloadUrl\": \"https://firebasestorage.googleapis.com/v0/b/onearmyworld.appspot.com/o/uploads%2Fhowtos%2F038gjWgLjiyYknbEjDeI%2FIMG_20200605_142311.jpg?alt=media&token=c272c174-1adc-45af-967b-771adce7295d\",\n \"type\": \"image/jpeg\",\n \"fullPath\": \"uploads/howtos/038gjWgLjiyYknbEjDeI/IMG_20200605_142311.jpg\",\n \"updated\": \"2021-04-05T15:09:00.605Z\",\n \"size\": 124661,\n \"timeCreated\": \"2021-04-05T15:09:00.605Z\",\n \"contentType\": \"image/jpeg\"\n },\n \"comments\": [],\n \"moderatorFeedback\": \"\",\n \"steps\": [\n {\n \"title\": \"Measure the plastic sheet\",\n \"text\": \"For this step we need to measure our plastic sheet: Height, Width and Thickness. Our X-Carve machine works with the CAM Software EASEL, for me, the easiest software for CNC milling out there. \\n\\nThe cool thing about Easel (https://easel.inventables.com/) is that you can \\\"simulate\\\" your actual material and THEY EVEN HAVE HDPE 2-Colors in their cutting material lists!!\\n\\n\\n\",\n \"images\": [\n {\n \"fullPath\": \"uploads/howtos/pbo0Pe44aTngvlD04kGf/1.jpg\",\n \"name\": \"1.jpg\",\n \"size\": 74095,\n \"type\": \"image/jpeg\",\n \"timeCreated\": \"2021-03-26T19:42:05.766Z\",\n \"contentType\": \"image/jpeg\",\n \"downloadUrl\": \"https://firebasestorage.googleapis.com/v0/b/onearmyworld.appspot.com/o/uploads%2Fhowtos%2Fpbo0Pe44aTngvlD04kGf%2F1.jpg?alt=media&token=293d733d-05a5-494a-9340-47f4564f1939\",\n \"updated\": \"2021-03-26T19:42:05.766Z\",\n \"src\": \"/resources/howtos/cut-out-shapes-out-of-plastic-sheets-with-a-cnc-/1.jpg\",\n \"alt\": \"1.jpg\"\n },\n {\n \"contentType\": \"image/jpeg\",\n \"timeCreated\": \"2021-03-26T19:42:05.669Z\",\n \"updated\": \"2021-03-26T19:42:05.669Z\",\n \"size\": 69665,\n \"downloadUrl\": \"https://firebasestorage.googleapis.com/v0/b/onearmyworld.appspot.com/o/uploads%2Fhowtos%2Fpbo0Pe44aTngvlD04kGf%2F2.jpg?alt=media&token=004f50f1-97ac-4df4-9ba9-f463aa4cbca3\",\n \"fullPath\": \"uploads/howtos/pbo0Pe44aTngvlD04kGf/2.jpg\",\n \"name\": \"2.jpg\",\n \"type\": \"image/jpeg\",\n \"src\": \"/resources/howtos/cut-out-shapes-out-of-plastic-sheets-with-a-cnc-/2.jpg\",\n \"alt\": \"2.jpg\"\n }\n ],\n \"_animationKey\": \"unique1\"\n },\n {\n \"text\": \"Using the CNC clamps from the X-Carve, secure the sheet to the table, \",\n \"_animationKey\": \"unique2\",\n \"images\": [\n {\n \"updated\": \"2021-03-26T19:42:06.249Z\",\n \"size\": 55544,\n \"fullPath\": \"uploads/howtos/pbo0Pe44aTngvlD04kGf/3.jpg\",\n \"timeCreated\": \"2021-03-26T19:42:06.249Z\",\n \"name\": \"3.jpg\",\n \"downloadUrl\": \"https://firebasestorage.googleapis.com/v0/b/onearmyworld.appspot.com/o/uploads%2Fhowtos%2Fpbo0Pe44aTngvlD04kGf%2F3.jpg?alt=media&token=0b9c1914-1c75-429e-b34a-1e2b3706edef\",\n \"contentType\": \"image/jpeg\",\n \"type\": \"image/jpeg\",\n \"src\": \"/resources/howtos/cut-out-shapes-out-of-plastic-sheets-with-a-cnc-/3.jpg\",\n \"alt\": \"3.jpg\"\n }\n ],\n \"title\": \"Secure sheet \"\n },\n {\n \"title\": \"Choosing a file to cut \",\n \"text\": \"Now we go to our illustrator, such as Inkscape to design a vector file or download and open source one frome https://thenounproject.com/.\\n\\nWe download the SVG file, which is an open source vector format and import it to Easel. \\n\",\n \"images\": [\n {\n \"downloadUrl\": \"https://firebasestorage.googleapis.com/v0/b/onearmyworld.appspot.com/o/uploads%2Fhowtos%2Fpbo0Pe44aTngvlD04kGf%2F4.jpg?alt=media&token=1cd2d49d-9335-4bb1-ac2a-e625322ca604\",\n \"contentType\": \"image/jpeg\",\n \"timeCreated\": \"2021-03-26T19:42:06.727Z\",\n \"updated\": \"2021-03-26T19:42:06.727Z\",\n \"name\": \"4.jpg\",\n \"size\": 42952,\n \"type\": \"image/jpeg\",\n \"fullPath\": \"uploads/howtos/pbo0Pe44aTngvlD04kGf/4.jpg\",\n \"src\": \"/resources/howtos/cut-out-shapes-out-of-plastic-sheets-with-a-cnc-/4.jpg\",\n \"alt\": \"4.jpg\"\n },\n {\n \"size\": 69255,\n \"fullPath\": \"uploads/howtos/pbo0Pe44aTngvlD04kGf/5.jpg\",\n \"updated\": \"2021-03-26T19:42:06.833Z\",\n \"timeCreated\": \"2021-03-26T19:42:06.833Z\",\n \"downloadUrl\": \"https://firebasestorage.googleapis.com/v0/b/onearmyworld.appspot.com/o/uploads%2Fhowtos%2Fpbo0Pe44aTngvlD04kGf%2F5.jpg?alt=media&token=7cca786a-7d47-43bb-900b-b8d101c276b4\",\n \"name\": \"5.jpg\",\n \"contentType\": \"image/jpeg\",\n \"type\": \"image/jpeg\",\n \"src\": \"/resources/howtos/cut-out-shapes-out-of-plastic-sheets-with-a-cnc-/5.jpg\",\n \"alt\": \"5.jpg\"\n }\n ],\n \"_animationKey\": \"unique3\"\n },\n {\n \"text\": \"Now with the file we can choose the width we want to carve/cut and then we go to cut and start the wizzard:\\n- We check that the sheet is fixed.\\n- We also specify the cutting bit, we are using a 1/8 flat flute bit. \\n- We tell the machine where the coordinate 0-0 is, which we always choose as the down left corner.\\n- We raise the bit, turn on the Router!!!\\n\\nAND PUM THE MAGIC BEGINS!!\",\n \"title\": \"Follow the cutting Wizzard\",\n \"images\": [\n {\n \"timeCreated\": \"2021-03-26T19:42:07.493Z\",\n \"size\": 72226,\n \"fullPath\": \"uploads/howtos/pbo0Pe44aTngvlD04kGf/6.jpg\",\n \"updated\": \"2021-03-26T19:42:07.493Z\",\n \"downloadUrl\": \"https://firebasestorage.googleapis.com/v0/b/onearmyworld.appspot.com/o/uploads%2Fhowtos%2Fpbo0Pe44aTngvlD04kGf%2F6.jpg?alt=media&token=ba7195dd-7771-435f-a188-057457697332\",\n \"contentType\": \"image/jpeg\",\n \"type\": \"image/jpeg\",\n \"name\": \"6.jpg\",\n \"src\": \"/resources/howtos/cut-out-shapes-out-of-plastic-sheets-with-a-cnc-/6.jpg\",\n \"alt\": \"6.jpg\"\n },\n {\n \"fullPath\": \"uploads/howtos/pbo0Pe44aTngvlD04kGf/7.jpg\",\n \"size\": 52424,\n \"downloadUrl\": \"https://firebasestorage.googleapis.com/v0/b/onearmyworld.appspot.com/o/uploads%2Fhowtos%2Fpbo0Pe44aTngvlD04kGf%2F7.jpg?alt=media&token=a3d5820c-cfe2-484e-8f76-f861ab8b756d\",\n \"contentType\": \"image/jpeg\",\n \"type\": \"image/jpeg\",\n \"timeCreated\": \"2021-03-26T19:42:07.308Z\",\n \"updated\": \"2021-03-26T19:42:07.308Z\",\n \"name\": \"7.jpg\",\n \"src\": \"/resources/howtos/cut-out-shapes-out-of-plastic-sheets-with-a-cnc-/7.jpg\",\n \"alt\": \"7.jpg\"\n },\n {\n \"fullPath\": \"uploads/howtos/pbo0Pe44aTngvlD04kGf/8.jpg\",\n \"name\": \"8.jpg\",\n \"type\": \"image/jpeg\",\n \"timeCreated\": \"2021-03-26T19:42:07.346Z\",\n \"size\": 55264,\n \"contentType\": \"image/jpeg\",\n \"downloadUrl\": \"https://firebasestorage.googleapis.com/v0/b/onearmyworld.appspot.com/o/uploads%2Fhowtos%2Fpbo0Pe44aTngvlD04kGf%2F8.jpg?alt=media&token=1c9816d7-3a99-4f41-8d3c-acc2670240f6\",\n \"updated\": \"2021-03-26T19:42:07.346Z\",\n \"src\": \"/resources/howtos/cut-out-shapes-out-of-plastic-sheets-with-a-cnc-/8.jpg\",\n \"alt\": \"8.jpg\"\n }\n ],\n \"_animationKey\": \"uniquenisc2v\"\n },\n {\n \"text\": \"You take now your glasses or object and postprocess them and of course show it to your friends, family and so on.\\n\\n\\n\",\n \"images\": [\n {\n \"fullPath\": \"uploads/howtos/pbo0Pe44aTngvlD04kGf/9.jpg\",\n \"contentType\": \"image/jpeg\",\n \"timeCreated\": \"2021-03-26T19:42:08.147Z\",\n \"downloadUrl\": \"https://firebasestorage.googleapis.com/v0/b/onearmyworld.appspot.com/o/uploads%2Fhowtos%2Fpbo0Pe44aTngvlD04kGf%2F9.jpg?alt=media&token=4dcfe37d-e1ad-41e5-a590-40b4c37c5e1a\",\n \"name\": \"9.jpg\",\n \"updated\": \"2021-03-26T19:42:08.147Z\",\n \"type\": \"image/jpeg\",\n \"size\": 82214,\n \"src\": \"/resources/howtos/cut-out-shapes-out-of-plastic-sheets-with-a-cnc-/9.jpg\",\n \"alt\": \"9.jpg\"\n }\n ],\n \"_animationKey\": \"uniquesgl34\",\n \"title\": \"Post-production and show case\"\n },\n {\n \"_animationKey\": \"uniquem4y0yi\",\n \"title\": \"Hack it and try it yourself\",\n \"text\": \"You can try this project with other types of CNC machines, even manual Routers or manual saw, as I did on this video: https://youtu.be/gxkcffQD3eQ, but the important thing is that you share what you do and help this community to grow!!!\\n\\nShare your ideas and comments!\",\n \"images\": [\n {\n \"contentType\": \"image/jpeg\",\n \"timeCreated\": \"2021-04-05T15:09:01.445Z\",\n \"fullPath\": \"uploads/howtos/038gjWgLjiyYknbEjDeI/IMG_20200605_142311.jpg\",\n \"type\": \"image/jpeg\",\n \"downloadUrl\": \"https://firebasestorage.googleapis.com/v0/b/onearmyworld.appspot.com/o/uploads%2Fhowtos%2F038gjWgLjiyYknbEjDeI%2FIMG_20200605_142311.jpg?alt=media&token=f94152ff-f923-4054-a3ad-d8ec588856fa\",\n \"size\": 124661,\n \"updated\": \"2021-04-05T15:09:01.445Z\",\n \"name\": \"IMG_20200605_142311.jpg\",\n \"src\": \"/resources/howtos/cut-out-shapes-out-of-plastic-sheets-with-a-cnc-/IMG_20200605_142311.jpg\",\n \"alt\": \"IMG_20200605_142311.jpg\"\n }\n ]\n }\n ],\n \"moderation\": \"accepted\",\n \"user\": {\n \"_modified\": \"2024-01-08T13:28:33.484Z\",\n \"_id\": \"gus-merckel\",\n \"subType\": \"mix\",\n \"moderation\": \"accepted\",\n \"_deleted\": false,\n \"verified\": false,\n \"type\": \"workspace\",\n \"location\": {\n \"lat\": 19.3935,\n \"lng\": -99.1656\n },\n \"_created\": \"2024-01-08T13:28:33.484Z\",\n \"geo\": {\n \"latitude\": 19.3935,\n \"lookupSource\": \"coordinates\",\n \"longitude\": -99.1656,\n \"localityLanguageRequested\": \"en\",\n \"continent\": \"North America\",\n \"continentCode\": \"NA\",\n \"countryName\": \"Mexico\",\n \"countryCode\": \"MX\",\n \"principalSubdivision\": \"Ciudad de Mexico\",\n \"principalSubdivisionCode\": \"MX-CMX\",\n \"city\": \"Mexico City\",\n \"locality\": \"Benito Juarez\",\n \"postcode\": \"03103\",\n \"plusCode\": \"76F29RVM+CQ\",\n \"localityInfo\": {\n \"administrative\": [\n {\n \"name\": \"Mexico\",\n \"description\": \"country in North America\",\n \"isoName\": \"Mexico\",\n \"order\": 2,\n \"adminLevel\": 2,\n \"isoCode\": \"MX\",\n \"wikidataId\": \"Q96\",\n \"geonameId\": 3996063\n },\n {\n \"name\": \"Mexico City\",\n \"description\": \"capital and largest city of Mexico\",\n \"order\": 5,\n \"adminLevel\": 4,\n \"wikidataId\": \"Q1489\",\n \"geonameId\": 3530597\n },\n {\n \"name\": \"Ciudad de Mexico\",\n \"description\": \"capital and largest city of Mexico\",\n \"isoName\": \"Ciudad de Mexico\",\n \"order\": 6,\n \"adminLevel\": 4,\n \"isoCode\": \"MX-CMX\",\n \"wikidataId\": \"Q1489\",\n \"geonameId\": 3527646\n },\n {\n \"name\": \"Benito Juarez\",\n \"description\": \"territorial demarcation of the Mexico City in Mexico\",\n \"order\": 7,\n \"adminLevel\": 6,\n \"wikidataId\": \"Q2356998\",\n \"geonameId\": 3827406\n }\n ],\n \"informative\": [\n {\n \"name\": \"North America\",\n \"description\": \"continent and northern subcontinent of the Americas\",\n \"isoName\": \"North America\",\n \"order\": 1,\n \"isoCode\": \"NA\",\n \"wikidataId\": \"Q49\",\n \"geonameId\": 6255149\n },\n {\n \"name\": \"America/Mexico_City\",\n \"description\": \"time zone\",\n \"order\": 3\n },\n {\n \"name\": \"Greater Mexico City\",\n \"description\": \"geographical object\",\n \"order\": 4,\n \"wikidataId\": \"Q665894\"\n },\n {\n \"name\": \"03103\",\n \"description\": \"postal code\",\n \"order\": 8\n }\n ]\n }\n },\n \"data\": {\n \"urls\": [\n {\n \"name\": \"Email\",\n \"url\": \"mailto:gustavomerckel@gmail.com\"\n },\n {\n \"name\": \"Facebook\",\n \"url\": \"https://www.facebook.com/pl%c3%a1stico-chido-110888520718193\"\n },\n {\n \"name\": \"sponsor the work\",\n \"url\": \"https://www.patreon.com/one_army\"\n }\n ],\n \"description\": \"Plástico Chido builds and modifies the PP machines, and also experiments with CNC and Laser Cut\",\n \"services\": [\n {\n \"welding\": false,\n \"assembling\": false,\n \"machining\": false,\n \"electronics\": false,\n \"molds\": false\n }\n ],\n \"title\": \"Plástico Chido\",\n \"images\": []\n },\n \"detail\": {\n \"services\": [],\n \"urls\": []\n }\n },\n \"category\": {\n \"label\": \"uncategorized\"\n }\n}"
}
],
- "tools": [
- {
- "type": "function",
- "function": {
- "name": "remove_file",
- "description": "Remove a file at given path",
- "parameters": {
- "type": "object",
- "properties": {
- "path": {
- "type": "string"
- }
- },
- "required": [
- "path"
- ]
- }
- }
- },
- {
- "type": "function",
- "function": {
- "name": "rename_file",
- "description": "Rename or move a file or directory",
- "parameters": {
- "type": "object",
- "properties": {
- "src": {
- "type": "string"
- },
- "dst": {
- "type": "string"
- }
- },
- "required": [
- "path"
- ]
- }
- }
- },
- {
- "type": "function",
- "function": {
- "name": "modify_project_files",
- "description": "Create or modify existing project files in one shot, preferably used for creating project structure)",
- "parameters": {
- "type": "object",
- "properties": {
- "files": {
- "type": "array",
- "items": {
- "type": "object",
- "properties": {
- "path": {
- "type": "string"
- },
- "content": {
- "type": "string",
- "description": "base64 encoded string"
- }
- },
- "required": [
- "path",
- "content"
- ]
- }
- }
- },
- "required": [
- "files"
- ]
- }
- }
- },
- {
- "type": "function",
- "function": {
- "name": "write_file",
- "description": "Writes to a file, given a path and content (base64). No directory or file exists check needed!",
- "parameters": {
- "type": "object",
- "properties": {
- "file": {
- "type": "object",
- "properties": {
- "path": {
- "type": "string"
- },
- "content": {
- "type": "string",
- "description": "base64 encoded string"
- }
- }
- }
- },
- "required": [
- "file"
- ]
- }
- }
- }
- ],
- "tool_choice": "auto",
- "parallel_tool_calls": false
+ "tools": []
}
\ No newline at end of file
diff --git a/src/model/howto-model.ts b/src/model/howto-model.ts
new file mode 100644
index 0000000..faf0c93
--- /dev/null
+++ b/src/model/howto-model.ts
@@ -0,0 +1,177 @@
+export const ITEM_TYPE = 'howto'
+////////////////////////////////
+//
+// Interfaces - Old
+export interface IHowto {
+ _createdBy: string
+ mentions: any[]
+ _deleted: boolean
+ fileLink: string
+ slug: string
+ _modified: string
+ previousSlugs: string[]
+ _created: string
+ description: string
+ votedUsefulBy: string[]
+ creatorCountry: string
+ total_downloads: number
+ title: string
+ time: string
+ files: any[]
+ category: IOACategory
+ difficulty_level: string
+ _id: string
+ tags?: IOATag[]
+ total_views: number
+ _contentModifiedTimestamp: string
+ cover_image: Image
+ comments: any[]
+ moderatorFeedback: string
+ steps: Step[]
+ moderation: string
+ user?: User
+}
+
+export interface Tags {
+ [key: string]: boolean
+}
+
+export interface CoverImage {
+ name: string
+ downloadUrl: string
+ type: string
+ fullPath: string
+ updated: string
+ size: number
+ timeCreated: string
+ contentType: string
+}
+
+export interface Step {
+ title: string
+ text: string
+ images: Image[]
+ _animationKey: string
+}
+export interface Image {
+ updated: string
+ size: number
+ fullPath: string
+ timeCreated: string
+ name: string
+ downloadUrl: string
+ contentType: string
+ type: string
+ src: string,
+ alt: string
+}
+
+/// Taxonomy
+export interface IOACategory {
+ _created: string
+ _id: string
+ _deleted: boolean
+ label: string
+ _modified: string
+}
+
+export interface IOATag {
+ categories: string[]
+ image: string
+ _created: string
+ _deleted: boolean
+ label: string
+ _createdBy: string
+ _modified: string
+ _id: string
+}
+
+export interface User {
+ _modified: string
+ _id: string
+ subType: string
+ moderation: string
+ _deleted: boolean
+ verified: boolean
+ type: string
+ location: Location
+ _created: string
+ geo: Geo
+ data: Data
+ detail: Detail
+}
+
+export interface Location {
+ lat: number
+ lng: number
+}
+
+export interface Geo {
+ latitude: number
+ lookupSource: string
+ longitude: number
+ localityLanguageRequested: string
+ continent: string
+ continentCode: string
+ countryName: string
+ countryCode: string
+ principalSubdivision: string
+ principalSubdivisionCode: string
+ city: string
+ locality: string
+ postcode: string
+ plusCode: string
+ localityInfo: LocalityInfo
+}
+
+export interface LocalityInfo {
+ administrative: Administrative[]
+ informative: Informative[]
+}
+
+export interface Administrative {
+ name: string
+ description: string
+ isoName?: string
+ order: number
+ adminLevel: number
+ isoCode?: string
+ wikidataId: string
+ geonameId: number
+}
+
+export interface Informative {
+ name: string
+ description: string
+ isoName?: string
+ order: number
+ isoCode?: string
+ wikidataId?: string
+ geonameId?: number
+}
+
+export interface Data {
+ urls: Url[]
+ description: string
+ services: Service[]
+ title: string
+ images: any[]
+}
+
+export interface Url {
+ name: string
+ url: string
+}
+
+export interface Service {
+ welding: boolean
+ assembling: boolean
+ machining: boolean
+ electronics: boolean
+ molds: boolean
+}
+
+export interface Detail {
+ services: any[]
+ urls: any[]
+}
diff --git a/src/model/howto-workflow.sh b/src/model/howto-workflow.sh
new file mode 100644
index 0000000..48bdfe4
--- /dev/null
+++ b/src/model/howto-workflow.sh
@@ -0,0 +1,11 @@
+kbotd --preferences ./todos-workflow.md \
+ --include=./howto-model.ts \
+ --include=./howto_sample.json \
+ --disable=terminal,git,npm,user,interact,search,email,web \
+ --disableTools=read_file,read_files,list_files,file_exists,web \
+ --model=anthropic/claude-3.7-sonnet:thinking \
+ --mode=completion \
+ --filters=code \
+ --dst=./json-ld-howto.ts
+
+
diff --git a/src/model/howto.ts b/src/model/howto.ts
index 499ffb2..8fa7fcd 100644
--- a/src/model/howto.ts
+++ b/src/model/howto.ts
@@ -5,10 +5,12 @@ import { sync as read } from '@polymech/fs/read'
import { sync as exists } from '@polymech/fs/exists'
import { sync as mkdir } from '@polymech/fs/dir'
import { sync as rm } from '@polymech/fs/remove'
-
+import { IHowto, Image } from './howto-model.js';
import type { Loader, LoaderContext } from 'astro/loaders'
import { sanitizeFilename } from "@polymech/fs/utils"
import { filter as language } from "@/base/kbot.js";
+export * from './howto-model.js'
+
import {
HOWTO_FILES_WEB,
HOWTO_FILES_ABS,
@@ -35,8 +37,8 @@ export const ITEM_TYPE = 'howto'
//export const load = () => get(`${HOWTO_ROOT()}/${HOWTO_GLOB}`, HOWTO_ROOT(), ITEM_TYPE)
export const item_path = (item: any) => `${HOWTO_ROOT()}/${item.data.slug}`
-const blacklist = [];
-const blacklist_mafia = ['precious-plastic', 'fair-enough', 'mad-plastic-labs', 'the-flipflopi', 'easymoulds', 'plasticpreneur', 'sustainable-design-studio'];
+const blacklist_ = [];
+const blacklist = ['precious-plastic', 'fair-enough', 'mad-plastic-labs', 'the-flipflopi', 'easymoulds', 'plasticpreneur', 'sustainable-design-studio'];
const download = async (url, outputPath) => {
const stream = createWriteStream(outputPath);
@@ -141,7 +143,7 @@ export const howtos = async () => {
label: 'uncategorized'
}
})
- howtos = howtos.filter((h) => {
+ howtos = howtos.filter((h:IHowto) => {
return h.steps.length > 0 && !blacklist.includes(h._createdBy);
});
return howtos
@@ -297,179 +299,3 @@ export async function applyFilters(text: string): Promise {
return filtered;
}
-////////////////////////////////
-//
-// Interfaces - Old
-export interface IHowto {
- _createdBy: string
- mentions: any[]
- _deleted: boolean
- fileLink: string
- slug: string
- _modified: string
- previousSlugs: string[]
- _created: string
- description: string
- votedUsefulBy: string[]
- creatorCountry: string
- total_downloads: number
- title: string
- time: string
- files: any[]
- category: IOACategory
- difficulty_level: string
- _id: string
- tags?: Tags
- total_views: number
- _contentModifiedTimestamp: string
- cover_image: Image
- comments: any[]
- moderatorFeedback: string
- steps: Step[]
- moderation: string
- user?: User
-}
-
-export interface Tags {
- [key: string]: boolean
-}
-
-export interface CoverImage {
- name: string
- downloadUrl: string
- type: string
- fullPath: string
- updated: string
- size: number
- timeCreated: string
- contentType: string
-}
-
-export interface Step {
- title: string
- text: string
- images: Image[]
- _animationKey: string
-}
-export interface Image {
- updated: string
- size: number
- fullPath: string
- timeCreated: string
- name: string
- downloadUrl: string
- contentType: string
- type: string
- src: string,
- alt: string
-}
-
-/// Taxonomy
-export interface IOACategory {
- _created: string
- _id: string
- _deleted: boolean
- label: string
- _modified: string
-}
-
-export interface IOATag {
- categories: string[]
- image: string
- _created: string
- _deleted: boolean
- label: string
- _createdBy: string
- _modified: string
- _id: string
-}
-
-export interface User {
- _modified: string
- _id: string
- subType: string
- moderation: string
- _deleted: boolean
- verified: boolean
- type: string
- location: Location
- _created: string
- geo: Geo
- data: Data
- detail: Detail
-}
-
-export interface Location {
- lat: number
- lng: number
-}
-
-export interface Geo {
- latitude: number
- lookupSource: string
- longitude: number
- localityLanguageRequested: string
- continent: string
- continentCode: string
- countryName: string
- countryCode: string
- principalSubdivision: string
- principalSubdivisionCode: string
- city: string
- locality: string
- postcode: string
- plusCode: string
- localityInfo: LocalityInfo
-}
-
-export interface LocalityInfo {
- administrative: Administrative[]
- informative: Informative[]
-}
-
-export interface Administrative {
- name: string
- description: string
- isoName?: string
- order: number
- adminLevel: number
- isoCode?: string
- wikidataId: string
- geonameId: number
-}
-
-export interface Informative {
- name: string
- description: string
- isoName?: string
- order: number
- isoCode?: string
- wikidataId?: string
- geonameId?: number
-}
-
-export interface Data {
- urls: Url[]
- description: string
- services: Service[]
- title: string
- images: any[]
-}
-
-export interface Url {
- name: string
- url: string
-}
-
-export interface Service {
- welding: boolean
- assembling: boolean
- machining: boolean
- electronics: boolean
- molds: boolean
-}
-
-export interface Detail {
- services: any[]
- urls: any[]
-}
diff --git a/src/model/howto_sample.js b/src/model/howto_sample.js
deleted file mode 100644
index edd4baa..0000000
--- a/src/model/howto_sample.js
+++ /dev/null
@@ -1,339 +0,0 @@
-{
- "_createdBy": "gus-merckel",
- "mentions": [],
- "_deleted": false,
- "fileLink": "",
- "slug": "cut-out-shapes-out-of-plastic-sheets-with-a-cnc-",
- "_modified": "2023-10-27T18:09:36.519Z",
- "previousSlugs": [
- "cut-out-shapes-out-of-plastic-sheets-with-a-cnc-"
- ],
- "_created": "2023-08-23T18:20:09.098Z",
- "description": "In this how to, I will show you our process to cut HDPE Sheets using a X-Carve CNC.\n\nHere is the full video in spanish with subtitles https://www.youtube.com/watch?v=4LrrFz802To ",
- "votedUsefulBy": [
- "sigolene",
- "mattia",
- "uillinoispreciousplastics"
- ],
- "creatorCountry": "mx",
- "total_downloads": 0,
- "title": "Cut out shapes out of plastic sheets with a CNC ",
- "time": "< 5 hours",
- "files": [],
- "difficulty_level": "Medium",
- "_id": "038gjWgLjiyYknbEjDeI",
- "tags": [
- "HDPE"
- ],
- "total_views": 232,
- "_contentModifiedTimestamp": "2023-08-23T18:20:09.098Z",
- "cover_image": {
- "name": "IMG_20200605_142311.jpg",
- "downloadUrl": "https://firebasestorage.googleapis.com/v0/b/onearmyworld.appspot.com/o/uploads%2Fhowtos%2F038gjWgLjiyYknbEjDeI%2FIMG_20200605_142311.jpg?alt=media&token=c272c174-1adc-45af-967b-771adce7295d",
- "type": "image/jpeg",
- "fullPath": "uploads/howtos/038gjWgLjiyYknbEjDeI/IMG_20200605_142311.jpg",
- "updated": "2021-04-05T15:09:00.605Z",
- "size": 124661,
- "timeCreated": "2021-04-05T15:09:00.605Z",
- "contentType": "image/jpeg"
- },
- "comments": [],
- "moderatorFeedback": "",
- "steps": [
- {
- "title": "Measure the plastic sheet",
- "text": "For this step we need to measure our plastic sheet: Height, Width and Thickness. Our X-Carve machine works with the CAM Software EASEL, for me, the easiest software for CNC milling out there. \n\nThe cool thing about Easel (https://easel.inventables.com/) is that you can \"simulate\" your actual material and THEY EVEN HAVE HDPE 2-Colors in their cutting material lists!!\n\n\n",
- "images": [
- {
- "fullPath": "uploads/howtos/pbo0Pe44aTngvlD04kGf/1.jpg",
- "name": "1.jpg",
- "size": 74095,
- "type": "image/jpeg",
- "timeCreated": "2021-03-26T19:42:05.766Z",
- "contentType": "image/jpeg",
- "downloadUrl": "https://firebasestorage.googleapis.com/v0/b/onearmyworld.appspot.com/o/uploads%2Fhowtos%2Fpbo0Pe44aTngvlD04kGf%2F1.jpg?alt=media&token=293d733d-05a5-494a-9340-47f4564f1939",
- "updated": "2021-03-26T19:42:05.766Z",
- "src": "/resources/howtos/cut-out-shapes-out-of-plastic-sheets-with-a-cnc-/1.jpg",
- "alt": "1.jpg"
- },
- {
- "contentType": "image/jpeg",
- "timeCreated": "2021-03-26T19:42:05.669Z",
- "updated": "2021-03-26T19:42:05.669Z",
- "size": 69665,
- "downloadUrl": "https://firebasestorage.googleapis.com/v0/b/onearmyworld.appspot.com/o/uploads%2Fhowtos%2Fpbo0Pe44aTngvlD04kGf%2F2.jpg?alt=media&token=004f50f1-97ac-4df4-9ba9-f463aa4cbca3",
- "fullPath": "uploads/howtos/pbo0Pe44aTngvlD04kGf/2.jpg",
- "name": "2.jpg",
- "type": "image/jpeg",
- "src": "/resources/howtos/cut-out-shapes-out-of-plastic-sheets-with-a-cnc-/2.jpg",
- "alt": "2.jpg"
- }
- ],
- "_animationKey": "unique1"
- },
- {
- "text": "Using the CNC clamps from the X-Carve, secure the sheet to the table, ",
- "_animationKey": "unique2",
- "images": [
- {
- "updated": "2021-03-26T19:42:06.249Z",
- "size": 55544,
- "fullPath": "uploads/howtos/pbo0Pe44aTngvlD04kGf/3.jpg",
- "timeCreated": "2021-03-26T19:42:06.249Z",
- "name": "3.jpg",
- "downloadUrl": "https://firebasestorage.googleapis.com/v0/b/onearmyworld.appspot.com/o/uploads%2Fhowtos%2Fpbo0Pe44aTngvlD04kGf%2F3.jpg?alt=media&token=0b9c1914-1c75-429e-b34a-1e2b3706edef",
- "contentType": "image/jpeg",
- "type": "image/jpeg",
- "src": "/resources/howtos/cut-out-shapes-out-of-plastic-sheets-with-a-cnc-/3.jpg",
- "alt": "3.jpg"
- }
- ],
- "title": "Secure sheet "
- },
- {
- "title": "Choosing a file to cut ",
- "text": "Now we go to our illustrator, such as Inkscape to design a vector file or download and open source one frome https://thenounproject.com/.\n\nWe download the SVG file, which is an open source vector format and import it to Easel. \n",
- "images": [
- {
- "downloadUrl": "https://firebasestorage.googleapis.com/v0/b/onearmyworld.appspot.com/o/uploads%2Fhowtos%2Fpbo0Pe44aTngvlD04kGf%2F4.jpg?alt=media&token=1cd2d49d-9335-4bb1-ac2a-e625322ca604",
- "contentType": "image/jpeg",
- "timeCreated": "2021-03-26T19:42:06.727Z",
- "updated": "2021-03-26T19:42:06.727Z",
- "name": "4.jpg",
- "size": 42952,
- "type": "image/jpeg",
- "fullPath": "uploads/howtos/pbo0Pe44aTngvlD04kGf/4.jpg",
- "src": "/resources/howtos/cut-out-shapes-out-of-plastic-sheets-with-a-cnc-/4.jpg",
- "alt": "4.jpg"
- },
- {
- "size": 69255,
- "fullPath": "uploads/howtos/pbo0Pe44aTngvlD04kGf/5.jpg",
- "updated": "2021-03-26T19:42:06.833Z",
- "timeCreated": "2021-03-26T19:42:06.833Z",
- "downloadUrl": "https://firebasestorage.googleapis.com/v0/b/onearmyworld.appspot.com/o/uploads%2Fhowtos%2Fpbo0Pe44aTngvlD04kGf%2F5.jpg?alt=media&token=7cca786a-7d47-43bb-900b-b8d101c276b4",
- "name": "5.jpg",
- "contentType": "image/jpeg",
- "type": "image/jpeg",
- "src": "/resources/howtos/cut-out-shapes-out-of-plastic-sheets-with-a-cnc-/5.jpg",
- "alt": "5.jpg"
- }
- ],
- "_animationKey": "unique3"
- },
- {
- "text": "Now with the file we can choose the width we want to carve/cut and then we go to cut and start the wizzard:\n- We check that the sheet is fixed.\n- We also specify the cutting bit, we are using a 1/8 flat flute bit. \n- We tell the machine where the coordinate 0-0 is, which we always choose as the down left corner.\n- We raise the bit, turn on the Router!!!\n\nAND PUM THE MAGIC BEGINS!!",
- "title": "Follow the cutting Wizzard",
- "images": [
- {
- "timeCreated": "2021-03-26T19:42:07.493Z",
- "size": 72226,
- "fullPath": "uploads/howtos/pbo0Pe44aTngvlD04kGf/6.jpg",
- "updated": "2021-03-26T19:42:07.493Z",
- "downloadUrl": "https://firebasestorage.googleapis.com/v0/b/onearmyworld.appspot.com/o/uploads%2Fhowtos%2Fpbo0Pe44aTngvlD04kGf%2F6.jpg?alt=media&token=ba7195dd-7771-435f-a188-057457697332",
- "contentType": "image/jpeg",
- "type": "image/jpeg",
- "name": "6.jpg",
- "src": "/resources/howtos/cut-out-shapes-out-of-plastic-sheets-with-a-cnc-/6.jpg",
- "alt": "6.jpg"
- },
- {
- "fullPath": "uploads/howtos/pbo0Pe44aTngvlD04kGf/7.jpg",
- "size": 52424,
- "downloadUrl": "https://firebasestorage.googleapis.com/v0/b/onearmyworld.appspot.com/o/uploads%2Fhowtos%2Fpbo0Pe44aTngvlD04kGf%2F7.jpg?alt=media&token=a3d5820c-cfe2-484e-8f76-f861ab8b756d",
- "contentType": "image/jpeg",
- "type": "image/jpeg",
- "timeCreated": "2021-03-26T19:42:07.308Z",
- "updated": "2021-03-26T19:42:07.308Z",
- "name": "7.jpg",
- "src": "/resources/howtos/cut-out-shapes-out-of-plastic-sheets-with-a-cnc-/7.jpg",
- "alt": "7.jpg"
- },
- {
- "fullPath": "uploads/howtos/pbo0Pe44aTngvlD04kGf/8.jpg",
- "name": "8.jpg",
- "type": "image/jpeg",
- "timeCreated": "2021-03-26T19:42:07.346Z",
- "size": 55264,
- "contentType": "image/jpeg",
- "downloadUrl": "https://firebasestorage.googleapis.com/v0/b/onearmyworld.appspot.com/o/uploads%2Fhowtos%2Fpbo0Pe44aTngvlD04kGf%2F8.jpg?alt=media&token=1c9816d7-3a99-4f41-8d3c-acc2670240f6",
- "updated": "2021-03-26T19:42:07.346Z",
- "src": "/resources/howtos/cut-out-shapes-out-of-plastic-sheets-with-a-cnc-/8.jpg",
- "alt": "8.jpg"
- }
- ],
- "_animationKey": "uniquenisc2v"
- },
- {
- "text": "You take now your glasses or object and postprocess them and of course show it to your friends, family and so on.\n\n\n",
- "images": [
- {
- "fullPath": "uploads/howtos/pbo0Pe44aTngvlD04kGf/9.jpg",
- "contentType": "image/jpeg",
- "timeCreated": "2021-03-26T19:42:08.147Z",
- "downloadUrl": "https://firebasestorage.googleapis.com/v0/b/onearmyworld.appspot.com/o/uploads%2Fhowtos%2Fpbo0Pe44aTngvlD04kGf%2F9.jpg?alt=media&token=4dcfe37d-e1ad-41e5-a590-40b4c37c5e1a",
- "name": "9.jpg",
- "updated": "2021-03-26T19:42:08.147Z",
- "type": "image/jpeg",
- "size": 82214,
- "src": "/resources/howtos/cut-out-shapes-out-of-plastic-sheets-with-a-cnc-/9.jpg",
- "alt": "9.jpg"
- }
- ],
- "_animationKey": "uniquesgl34",
- "title": "Post-production and show case"
- },
- {
- "_animationKey": "uniquem4y0yi",
- "title": "Hack it and try it yourself",
- "text": "You can try this project with other types of CNC machines, even manual Routers or manual saw, as I did on this video: https://youtu.be/gxkcffQD3eQ, but the important thing is that you share what you do and help this community to grow!!!\n\nShare your ideas and comments!",
- "images": [
- {
- "contentType": "image/jpeg",
- "timeCreated": "2021-04-05T15:09:01.445Z",
- "fullPath": "uploads/howtos/038gjWgLjiyYknbEjDeI/IMG_20200605_142311.jpg",
- "type": "image/jpeg",
- "downloadUrl": "https://firebasestorage.googleapis.com/v0/b/onearmyworld.appspot.com/o/uploads%2Fhowtos%2F038gjWgLjiyYknbEjDeI%2FIMG_20200605_142311.jpg?alt=media&token=f94152ff-f923-4054-a3ad-d8ec588856fa",
- "size": 124661,
- "updated": "2021-04-05T15:09:01.445Z",
- "name": "IMG_20200605_142311.jpg",
- "src": "/resources/howtos/cut-out-shapes-out-of-plastic-sheets-with-a-cnc-/IMG_20200605_142311.jpg",
- "alt": "IMG_20200605_142311.jpg"
- }
- ]
- }
- ],
- "moderation": "accepted",
- "user": {
- "_modified": "2024-01-08T13:28:33.484Z",
- "_id": "gus-merckel",
- "subType": "mix",
- "moderation": "accepted",
- "_deleted": false,
- "verified": false,
- "type": "workspace",
- "location": {
- "lat": 19.3935,
- "lng": -99.1656
- },
- "_created": "2024-01-08T13:28:33.484Z",
- "geo": {
- "latitude": 19.3935,
- "lookupSource": "coordinates",
- "longitude": -99.1656,
- "localityLanguageRequested": "en",
- "continent": "North America",
- "continentCode": "NA",
- "countryName": "Mexico",
- "countryCode": "MX",
- "principalSubdivision": "Ciudad de Mexico",
- "principalSubdivisionCode": "MX-CMX",
- "city": "Mexico City",
- "locality": "Benito Juarez",
- "postcode": "03103",
- "plusCode": "76F29RVM+CQ",
- "localityInfo": {
- "administrative": [
- {
- "name": "Mexico",
- "description": "country in North America",
- "isoName": "Mexico",
- "order": 2,
- "adminLevel": 2,
- "isoCode": "MX",
- "wikidataId": "Q96",
- "geonameId": 3996063
- },
- {
- "name": "Mexico City",
- "description": "capital and largest city of Mexico",
- "order": 5,
- "adminLevel": 4,
- "wikidataId": "Q1489",
- "geonameId": 3530597
- },
- {
- "name": "Ciudad de Mexico",
- "description": "capital and largest city of Mexico",
- "isoName": "Ciudad de Mexico",
- "order": 6,
- "adminLevel": 4,
- "isoCode": "MX-CMX",
- "wikidataId": "Q1489",
- "geonameId": 3527646
- },
- {
- "name": "Benito Juarez",
- "description": "territorial demarcation of the Mexico City in Mexico",
- "order": 7,
- "adminLevel": 6,
- "wikidataId": "Q2356998",
- "geonameId": 3827406
- }
- ],
- "informative": [
- {
- "name": "North America",
- "description": "continent and northern subcontinent of the Americas",
- "isoName": "North America",
- "order": 1,
- "isoCode": "NA",
- "wikidataId": "Q49",
- "geonameId": 6255149
- },
- {
- "name": "America/Mexico_City",
- "description": "time zone",
- "order": 3
- },
- {
- "name": "Greater Mexico City",
- "description": "geographical object",
- "order": 4,
- "wikidataId": "Q665894"
- },
- {
- "name": "03103",
- "description": "postal code",
- "order": 8
- }
- ]
- }
- },
- "data": {
- "urls": [
- {
- "name": "Email",
- "url": "mailto:gustavomerckel@gmail.com"
- },
- {
- "name": "Facebook",
- "url": "https://www.facebook.com/pl%c3%a1stico-chido-110888520718193"
- },
- {
- "name": "sponsor the work",
- "url": "https://www.patreon.com/one_army"
- }
- ],
- "description": "Plástico Chido builds and modifies the PP machines, and also experiments with CNC and Laser Cut",
- "services": [
- {
- "welding": false,
- "assembling": false,
- "machining": false,
- "electronics": false,
- "molds": false
- }
- ],
- "title": "Plástico Chido",
- "images": []
- },
- "detail": {
- "services": [],
- "urls": []
- }
- },
- "category": {
- "label": "uncategorized"
- }
-}
\ No newline at end of file
diff --git a/src/model/json-ld-howto.ts b/src/model/json-ld-howto.ts
new file mode 100644
index 0000000..805f506
--- /dev/null
+++ b/src/model/json-ld-howto.ts
@@ -0,0 +1,94 @@
+import { IHowto } from './howto-model.js';
+
+export const toJSONLD = (howto: IHowto) => {
+ const jsonLD: any = {
+ "@context": "https://schema.org",
+ "@type": "HowTo",
+ "name": howto.title,
+ "description": howto.description,
+ };
+
+ if (howto.cover_image?.downloadUrl) {
+ jsonLD.image = howto.cover_image.downloadUrl;
+ }
+
+ if (howto.time) {
+ let duration = "";
+ const timeMatch = howto.time.match(/(\d+)\s*(hour|minute|second|day|week|month|year)/i);
+ if (timeMatch) {
+ const value = parseInt(timeMatch[1], 10);
+ const unit = timeMatch[2].toLowerCase();
+
+ switch(unit) {
+ case 'hour': duration = `PT${value}H`; break;
+ case 'minute': duration = `PT${value}M`; break;
+ case 'second': duration = `PT${value}S`; break;
+ case 'day': duration = `P${value}D`; break;
+ case 'week': duration = `P${value}W`; break;
+ case 'month': duration = `P${value}M`; break;
+ case 'year': duration = `P${value}Y`; break;
+ }
+ } else if (howto.time.includes('<')) {
+ const lessTimeMatch = howto.time.match(/< (\d+)\s*(hour|minute|second|day|week|month|year)/i);
+ if (lessTimeMatch) {
+ const value = parseInt(lessTimeMatch[1], 10);
+ const unit = lessTimeMatch[2].toLowerCase();
+
+ switch(unit) {
+ case 'hour': duration = `PT${value - 1}H`; break;
+ case 'minute': duration = `PT${value - 1}M`; break;
+ case 'second': duration = `PT${value - 1}S`; break;
+ case 'day': duration = `P${value - 1}D`; break;
+ case 'week': duration = `P${value - 1}W`; break;
+ case 'month': duration = `P${value - 1}M`; break;
+ case 'year': duration = `P${value - 1}Y`; break;
+ }
+ }
+ }
+
+ if (duration) {
+ jsonLD.totalTime = duration;
+ }
+ }
+
+ if (howto.difficulty_level) {
+ jsonLD.difficultyLevel = howto.difficulty_level;
+ }
+
+ if (howto.category?.label) {
+ jsonLD.category = howto.category.label;
+ }
+
+ if (howto.tags && howto.tags.length > 0) {
+ const keywords = howto.tags.map(tag =>
+ typeof tag === 'string' ? tag : tag?.label || ''
+ ).filter(Boolean).join(', ');
+
+ if (keywords) {
+ jsonLD.keywords = keywords;
+ }
+ }
+
+ if (howto.steps && howto.steps.length > 0) {
+ jsonLD.step = howto.steps.map((step, index) => {
+ const stepObj: any = {
+ "@type": "HowToStep",
+ "name": step.title,
+ "text": step.text,
+ "position": index + 1
+ };
+
+ if (step.images && step.images.length > 0) {
+ if (step.images.length === 1) {
+ stepObj.image = step.images[0].downloadUrl;
+ } else {
+ stepObj.image = step.images.map(image => image.downloadUrl);
+ }
+ }
+
+ return stepObj;
+ });
+ }
+
+ return jsonLD;
+};
\ No newline at end of file
diff --git a/src/model/todos-workflow.md b/src/model/todos-workflow.md
new file mode 100644
index 0000000..9676743
--- /dev/null
+++ b/src/model/todos-workflow.md
@@ -0,0 +1,9 @@
+## Todos
+
+- we use Typescript, ESM, minimal dependencies, functional programming
+- dont document or comment, return just the code, ready to go
+- no additional dependencies
+
+## Todos
+
+- [ ] function, to convert a howto (howto-model.ts) to json-ld structured data, satisfying Google, "toJSON-LD"