Projects — biblioteca Constructor
Ghid pentru agenți (și autori) despre proiecte în Constructor: ce sunt, cum arată biblioteca, și cum se creează / modifică / șterg.
Nu inventa API — folosește doar metodele din DocStore / ProjectLibraryService și tipurile din @gstack/gds-core.
Ce este un „project”
Section titled “Ce este un „project””| Nivel | Tip | Rol |
|---|---|---|
| Library | LibraryFile | Întregul workspace multi-proiect: listă + proiect activ + revision. Persistat în localStorage și dual-write în PostgreSQL via GET/PUT /api/library. |
| Project | GdsProject (în WorkspaceSnapshot) | Un mockup: meta la nivel de proiect + pages[] (web) + screens[] (mobil). |
| Page / screen | GdsPage / GdsScreen | O pagină (sau ecran mobil) în proiect: titlu, sidenav opțional (doar pe page), blocks[]. |
Nu confunda:
- ștergerea unei pagini ≠ ștergerea unui proiect;
activePageId/activeScreenIdsunt cursor UI în snapshot, nu ID-ul proiectului;- biblioteca (
LibraryFile) ≠ un singurGdsProject.
Forma bibliotecii
Section titled “Forma bibliotecii”interface LibraryFile { version: 1; activeId: string; // id proiect deschis în editor order: string[]; // ordine afișare (cel mai nou primul) items: Record<string, WorkspaceSnapshot>; revision?: number; // monotonic; stale autosave guard (cross-tab)}
interface WorkspaceSnapshot { project: GdsProject; activePageId: string; activeScreenId: string; editSurface: 'page' | 'screen';}Cheie locală: gds.constructor.library.v1.
Sincronizare cross-tab: BroadcastChannel + evenimentul storage (același browser).
Durabilitate: ProjectLibraryService face dual-write — după fiecare persist() local, PUT /api/library (debounced) către Postgres; la boot face GET /api/library și merge după revision (nu șterge date locale dacă API/DB e jos).
Salvare explicită: butonul Salvează din tabstrip (app-tabs) → DocStore.saveProjectToDatabase() → PUT /api/projects/:id (JSON-ul proiectului activ din localStorage).
PostgreSQL (dev)
Section titled “PostgreSQL (dev)”# Pornește containerul (port host default 5432; dacă e ocupat: POSTGRES_PORT=5433)npm run db:up
# Variabile în `.env` (vezi `.env.example`): POSTGRES_* sau DATABASE_URL# API (:4202) citește aceleași variabile — restart `npm start` după db:upSchema: .database/init.sql (constructor_library = blob LibraryFile, constructor_projects = snapshot per proiect).
Health: GET /api/health → { ok: true, db: "up"|"down"|"disabled" }.
Per proiect: GET/PUT /api/projects/:id → { id, snapshot } / { id, snapshot, revision }.
Semnale utile pe ProjectLibraryService: activeId, summaries, entries, peerRevision, revision(), pushActiveProjectToRemote().
GdsProject — meta la nivel de proiect
Section titled “GdsProject — meta la nivel de proiect”Contract canonic: projects/gds-core/src/lib/blocks.ts → GdsProject.
interface GdsProject { version: 2; meta: { title: string; // numele proiectului (card / rename) product: string; // produs (sincronizat și din sidenav.product.title) accent: AccentName; // indigo|emerald|amber|rose|blue|teal|… font?: FontName; // default ibm-plex borderColor?: string; // opțional; omit → token default lang: Lang; // 'en' | 'ro' seed: number; gridCell?: number; boardWidthPx?: number; boardHeightPx?: number; deletedPageIds?: string[]; // tombstone la merge — nu reînvia pagini șterse }; pages: GdsPage[]; screens?: GdsScreen[]; // după normalize: întotdeauna ≥1}Accent / font / border aplicate global pe toate proiectele: DocStore.setGlobalAccent / setGlobalFont / setGlobalBorderColor → ProjectLibraryService.applyThemeToAll.
- Lista / carduri:
app-projects-page(projects-page.component.ts) — create, open, rename, delete, export HTML. - Switch activ:
DocStore.openProject(id)(flush autosave →library.open→applySnapshot).
Instrucțiuni agent — CREATE
Section titled “Instrucțiuni agent — CREATE”API real (preferat):
docStore.createProject();Flux: flushAutosave → library.create(lang) → applySnapshot.
Ce face ProjectLibraryService.create:
- Generează id
proj-<base36>-…. - Seed din
initialProject()— gol: o paginăHome, un screenScreen 1, fără sidenav / blocuri. - Meta default:
title: 'New mockup',product: 'Product', accent/font default,lang: 'en'(create forțează EN în intern). - Al 2-lea+ proiect: titlul devine
Mockup 2,Mockup 3, … - Setează
activeIdpe noul proiect, îl pune primul înorder,persist()(bumprevision+ localStorage + PUT debounced).
Gol vs seed GLog:
- Create din UI/library = gol (
initialProject), nu apeleazăbuildGlogProject. - GLog canonic:
buildGlogProject(lang)înglog.project.ts(ex.tools/seed-glog.ts). Dacămeta.title/meta.producteglog,normalizeProjectruleazărepairGlogProject(refresh demo blocks, drop pagină Settings veche). - Nu inventa un „create seeded” pe DocStore — nu există; seed-ul GLog e tool / repair, nu butonul „New project”.
Exemplu — înainte / după CREATE
Section titled “Exemplu — înainte / după CREATE”Înainte (un proiect activ):
{ "version": 1, "activeId": "proj-abc", "order": ["proj-abc"], "items": { "proj-abc": { "project": { "version": 2, "meta": { "title": "New mockup", "product": "Product", "accent": "indigo", "lang": "en", "seed": 42 }, "pages": [{ "id": "p1", "title": "Home", "sidenav": null, "blocks": [] }], "screens": [{ "id": "s1", "title": "Screen 1", "blocks": [] }] }, "activePageId": "p1", "activeScreenId": "s1", "editSurface": "page" } }, "revision": 3}După createProject():
{ "version": 1, "activeId": "proj-xyz", "order": ["proj-xyz", "proj-abc"], "items": { "proj-xyz": { "project": { "version": 2, "meta": { "title": "Mockup 2", "product": "Product", "accent": "indigo", "lang": "en", "seed": 123456789 }, "pages": [{ "id": "p…", "title": "Home", "sidenav": null, "blocks": [] }], "screens": [{ "id": "s…", "title": "Screen 1", "blocks": [] }] }, "activePageId": "p…", "activeScreenId": "s…", "editSurface": "page" }, "proj-abc": { "…": "neschimbat" } }, "revision": 4}Instrucțiuni agent — MODIFY
Section titled “Instrucțiuni agent — MODIFY”| Acțiune | API |
|---|---|
| Switch activ | docStore.openProject(id) → boolean (false dacă id necunoscut) |
| Rename | docStore.renameProject(id, title) — trim obligatoriu; pe activ apelează setTitle (cu history); pe inactiv library.rename |
| Meta titlu (activ) | docStore.setTitle(title) |
| Accent / font / border (doar activ) | setAccent / setFont / setBorderColor |
| Accent / font / border (toată library) | setGlobalAccent / setGlobalFont / setGlobalBorderColor |
| Board size | setBoardSize(w, h) |
| Salvare canvas activ | autosave → library.saveActive(snapshot, expectedRev?); false dacă expectedRev ≠ revision() (stale) |
saveActive nu șterge pagini sibling din library dacă DocStore vine cu mai puține pagini decât snapshot-ul existent (protecție wipe).
Exemplu — rename + switch
Section titled “Exemplu — rename + switch”docStore.renameProject('proj-xyz', 'Portal cetățean');docStore.openProject('proj-abc'); // salvează activul curent, apoi încarcă proj-abcDupă rename pe proj-xyz: items['proj-xyz'].project.meta.title === 'Portal cetățean' (și cardul din summaries / entries).
Instrucțiuni agent — DELETE
Section titled “Instrucțiuni agent — DELETE”docStore.deleteProject(id); // booleanReguli reale (library.delete):
- Refuză dacă rămâne un singur proiect (
Object.keys(items).length <= 1) →{ ok: false }. - Refuză dacă
idlipsește dinitems. - Scoate din
items+order; bumprevision; writelocalStorage; BroadcastChannel pe peeri. - Dacă proiectul șters era activ:
activeId = order[0], returnează snapshot-ul noului activ → DocStoreapplySnapshot. - Dacă șters ≠ activ: editorul rămâne pe proiectul curent (
switched: false).
UI: confirm dialog; butonul e no-op când entries.length <= 1.
Exemplu — înainte / după DELETE
Section titled “Exemplu — înainte / după DELETE”Înainte: order: ["proj-xyz", "proj-abc"], activeId: "proj-xyz".
După deleteProject('proj-xyz'):
{ "version": 1, "activeId": "proj-abc", "order": ["proj-abc"], "items": { "proj-abc": { "…": "…" } }, "revision": 5}După încercare de a șterge ultimul proiect: library neschimbată, deleteProject → false.
Soft rules (revision / merge)
Section titled “Soft rules (revision / merge)”revisioncrește la fiecarepersist/ delete / theme-all write.- Autosave cu
expectedRevdiferit de library → skip (nu scrie peste peer). - Cross-tab:
BroadcastChannel+storage→reloadFromLocalStorage+mergeProjectPages(reatașează pagini locale lipsă dacă peer le-a dropat fărădeletedPageIds). - Dedup pe titlu (case-insensitive) la normalize — evită duplicate GLog / seed.
- Nu „câștiga” o cursă inventând un
revisionmai mare manual în JSON — lasă library service să avanseze.
Ce să NU faci
Section titled “Ce să NU faci”- Nu șterge / golește
itemsla{}din agent — lasă cel puțin un proiect. - Nu inventa race pe
revision(scrieri directe în localStorage cu rev inventat). - Nu confunda proiect cu pagină:
addPage/ remove page ≠createProject/deleteProject. - Nu presupune că
create(lang)păstrează limba cerută — implementarea internă seed-uiește EN. - Nu aștepta ca chat AI să muteze library fără wiring — vezi mai jos.
- Nu inventa props pe componente GDS ca să „creezi un proiect”.
- Dual-write Postgres e opțional: fără DB, localStorage rămâne sursa de adevăr a sesiunii; cu
npm run db:up,GET/PUT /api/librarysincronizează acelașiLibraryFile, iar Salvează din tabstrip facePUT /api/projects/:id.
Chat AI — vizibilitate
Section titled “Chat AI — vizibilitate”AiAgentService trimite la /api/ai/chat:
{ messages, project: this.store.project(), // GdsProject activ (meta + pages + screens) selection: { selectedId, activePageId, activeScreenId, editSurface, activeProjectId, lang, }, library: { activeId, projects: [{ id, title }, …], // din projectSummaries — nu tot LibraryFile },}Acțiuni library expuse în chat (via ai-agent-actions / Kimi tools): create_project, open_project, rename_project.
Nu există delete_project în tool surface — ștergerea rămâne UI-only (DocStore.deleteProject).
Screens: paritate cu pages (select_screen / add_screen / rename_screen / remove_screen / duplicate_screen); add_block poate ținti screenId (mutually exclusive cu pageId).
Lang: set_lang → DocStore.setLang('en'|'ro').
Documentează totuși API-urile reale de mai sus: sursa de adevăr rămâne DocStore + ProjectLibraryService.
| Fișier | Rol |
|---|---|
projects/constructor/src/app/state/project-library.service.ts | Library: create, open, rename, delete, saveActive, pushActiveProjectToRemote, theme-all, revision |
projects/constructor/src/app/state/doc-store.ts | Editor: createProject, openProject, renameProject, deleteProject, saveProjectToDatabase, meta |
projects/constructor/src/app/state/document.factory.ts | initialProject, normalizeProject |
projects/constructor/src/app/state/glog.project.ts | buildGlogProject, repairGlogProject |
projects/constructor/src/app/builder/projects-page.component.ts | UI library |
projects/constructor/src/app/builder/tabs.component.ts | Tabstrip pagini + buton Salvează → DB |
projects/gds-core/src/lib/blocks.ts | GdsProject |
tools/api-server.mjs | /api/library, /api/projects/:id, /api/health, /api/ai/* |
tools/library-db.mjs | Layer Postgres (getLibrary / putLibrary / getProject / putProject) |