PageSourceSearch

https://pathcreatives.netlify.app/previews/project-state.js

js pathcreatives.netlify.app collected 2026-10-03 11:14:47 UTC 23,145 bytes, 785 lines download raw bytes

1/**
2 * Cross-step project state — sessionStorage cache + Supabase persistence.
3 */
4(function (global) {
5  const KEYS = {
6    url: 'pc:project-url',
7    company: 'pc:project-company',
8    offer: 'pc:project-offer',
9    slug: 'pc:project-slug',
10    scrape: 'pc:scrape',
11    draftBrief: 'pc:draft-brief',
12    approvedBrief: 'pc:approved-brief',
13    briefApproved: 'pc:brief-approved',
14    briefingSeen: 'pc:briefing-seen',
15    generateSeen: 'pc:generate-seen',
16    projectMode: 'pc:project-mode',
17    flowMode: 'pc:flow-mode',
18    assetsApprovedGlobal: 'pc:assets-approved',
19    imageryApprovedGlobal: 'pc:imagery-approved'
20  };
21
22  function slugify(value) {
23    return String(value || 'project')
24      .toLowerCase()
25      .replace(/[^a-z0-9]+/g, '-')
26      .replace(/^-+|-+$/g, '')
27      .slice(0, 48) || 'project';
28  }
29
30  function readJson(key) {
31    try {
32      const raw = sessionStorage.getItem(key);
33      return raw ? JSON.parse(raw) : null;
34    } catch {
35      return null;
36    }
37  }
38
39  function writeJson(key, value) {
40    sessionStorage.setItem(key, JSON.stringify(value));
41  }
42
43  function getUrl() {
44    return sessionStorage.getItem(KEYS.url) || '';
45  }
46
47  function setUrl(url) {
48    sessionStorage.setItem(KEYS.url, String(url || '').trim());
49  }
50
51  function getScrape() {
52    return readJson(KEYS.scrape);
53  }
54
55  function setScrape(scrape) {
56    if (scrape) writeJson(KEYS.scrape, scrape);
57    else sessionStorage.removeItem(KEYS.scrape);
58  }
59
60  function pickAssetArray(storedVal, fallbackVal) {
61    if (Array.isArray(storedVal) && storedVal.length) return storedVal;
62    if (Array.isArray(fallbackVal) && fallbackVal.length) return fallbackVal;
63    if (Array.isArray(storedVal)) return storedVal;
64    return Array.isArray(fallbackVal) ? fallbackVal : [];
65  }
66
67  function scrapeMetaFromScrape(scrape) {
68    if (!scrape) return {};
69    return {
70      url: scrape.url || getUrl(),
71      title: scrape.title || '',
72      description: scrape.description || '',
73      manual: Boolean(scrape.manual),
74      scrapedAt: scrape.scrapedAt || null
75    };
76  }
77
78  function brandKitFromScrape(scrape) {
79    if (!scrape) return null;
80    return {
81      colors: Array.isArray(scrape.colors) ? scrape.colors.map((c) => ({ ...c })) : [],
82      logos: Array.isArray(scrape.logos) ? scrape.logos.map((l) => ({ ...l })) : [],
83      pageImagery: Array.isArray(scrape.imagery) ? scrape.imagery.map((i) => ({ ...i })) : [],
84      scrapeMeta: scrapeMetaFromScrape(scrape)
85    };
86  }
87
88  function getCompany() {
89    return sessionStorage.getItem(KEYS.company) || '';
90  }
91
92  function setCompany(name) {
93    if (name) sessionStorage.setItem(KEYS.company, name);
94  }
95
96  function getOffer() {
97    return sessionStorage.getItem(KEYS.offer) || '';
98  }
99
100  function setOffer(name) {
101    if (name) sessionStorage.setItem(KEYS.offer, name);
102  }
103
104  function getSlug() {
105    const stored = sessionStorage.getItem(KEYS.slug);
106    if (stored) return stored;
107    const fromCompany = slugify(getCompany());
108    if (fromCompany !== 'project') {
109      sessionStorage.setItem(KEYS.slug, fromCompany);
110      return fromCompany;
111    }
112    return 'project';
113  }
114
115  function setSlug(slug) {
116    sessionStorage.setItem(KEYS.slug, slugify(slug));
117  }
118
119  function syncMetaFromBrief(brief) {
120    if (!brief) return;
121    if (brief.companyName) {
122      setCompany(brief.companyName);
123      setSlug(brief.companyName);
124    }
125    if (brief.offerName) setOffer(brief.offerName);
126    if (brief.url) setUrl(brief.url);
127  }
128
129  function stripBriefForStorage(brief) {
130    if (!brief) return null;
131    const { scrape, approved, ...rest } = brief;
132    return rest;
133  }
134
135  function getDraftBrief() {
136    return readJson(KEYS.draftBrief);
137  }
138
139  function setDraftBrief(brief) {
140    if (!brief) {
141      sessionStorage.removeItem(KEYS.draftBrief);
142      return;
143    }
144    writeJson(KEYS.draftBrief, stripBriefForStorage(brief));
145  }
146
147  function isBriefApproved() {
148    return sessionStorage.getItem(KEYS.briefApproved) === '1';
149  }
150
151  function getApprovedBrief() {
152    return readJson(KEYS.approvedBrief);
153  }
154
155  function setApprovedBrief(brief) {
156    if (!brief) {
157      sessionStorage.removeItem(KEYS.approvedBrief);
158      sessionStorage.removeItem(KEYS.briefApproved);
159      return;
160    }
161    writeJson(KEYS.approvedBrief, { ...stripBriefForStorage(brief), approved: true });
162    sessionStorage.setItem(KEYS.briefApproved, '1');
163    syncMetaFromBrief(brief);
164  }
165
166  function getProjectMode() {
167    return sessionStorage.getItem(KEYS.projectMode) || 'ai';
168  }
169
170  function setProjectMode(mode) {
171    sessionStorage.setItem(KEYS.projectMode, mode === 'demo' ? 'demo' : 'ai');
172  }
173
174  function isDemoMode() {
175    return getProjectMode() === 'demo';
176  }
177
178  function getFlowMode() {
179    return sessionStorage.getItem(KEYS.flowMode) || 'consolidated';
180  }
181
182  function setFlowMode(mode) {
183    const next = mode === 'layered' ? 'layered' : 'consolidated';
184    sessionStorage.setItem(KEYS.flowMode, next);
185  }
186
187  function isConsolidatedFlow() {
188    return getFlowMode() !== 'layered';
189  }
190
191  async function recordWizardStep(step, action, detail = {}) {
192    if (!global.PathCreativesSupabase) return null;
193    try {
194      await global.PathCreativesSupabase.init();
195      return global.PathCreativesSupabase.recordWizardStep(getSlug(), step, action, detail);
196    } catch (err) {
197      console.warn('[PathCreativesProject] recordWizardStep failed', err);
198      return null;
199    }
200  }
201
202  async function saveProjectHistory(entry = {}) {
203    const slug = getSlug();
204    const brief = getApprovedBrief() || {};
205    const company = entry.company || getCompany() || brief.companyName || slug;
206    const offer = entry.offer || getOffer() || brief.offerName || 'Offer';
207    const offerSlug = slugify(offer);
208
209    const payload = {
210      displayDate: entry.displayDate || new Date().toISOString().slice(0, 10),
211      sortDate: entry.sortDate || Number(new Date().toISOString().slice(0, 10).replace(/-/g, '')),
212      thumbnails: entry.thumbnails || [],
213      colors: entry.colors || [],
214      logoUrl: entry.logoUrl || null,
215      creatives: entry.creatives || [],
216      creativeCount: entry.creativeCount || (entry.creatives?.length ?? 4),
217      insights: entry.insights ?? 0,
218      publishedAt: entry.publishedAt || new Date().toISOString(),
219      status: entry.status || 'delivered',
220      snapshot: {
221        briefSummary: entry.briefSummary || getBriefOverview(brief),
222        briefStrategy: entry.briefStrategy || brief.brandContext?.strategy || '',
223        sourceUrl: entry.sourceUrl || getUrl() || brief.url,
224        company,
225        offer,
226        offerSlug,
227        mode: getProjectMode(),
228        ...(entry.snapshot || {})
229      }
230    };
231
232    if (global.PathCreativesHistory?.upsertLocalProject) {
233      try {
234        global.PathCreativesHistory.upsertLocalProject({
235          slug,
236          company,
237          offer,
238          offerSlug,
239          ...payload,
240          sourceUrl: payload.snapshot.sourceUrl,
241          briefSummary: payload.snapshot.briefSummary,
242          briefStrategy: payload.snapshot.briefStrategy
243        });
244      } catch (err) {
245        console.warn('[PathCreativesProject] local history save failed', err);
246      }
247    }
248
249    if (global.PathCreativesSupabase) {
250      try {
251        await global.PathCreativesSupabase.init();
252        await global.PathCreativesSupabase.saveHistory(slug, payload);
253        await global.PathCreativesSupabase.upsertProject({
254          slug,
255          name: company,
256          company,
257          offer,
258          offerSlug,
259          sourceUrl: payload.snapshot.sourceUrl,
260          status: payload.status,
261          briefSummary: payload.snapshot.briefSummary,
262          briefStrategy: payload.snapshot.briefStrategy,
263          creativeCount: payload.creativeCount,
264          insights: payload.insights
265        });
266      } catch (err) {
267        console.warn('[PathCreativesProject] saveProjectHistory failed', err);
268        return false;
269      }
270    }
271
272    return true;
273  }
274
275  async function ensureProjectRow(meta = {}) {
276    const slug = getSlug();
277    const company = meta.companyName || getCompany() || slug;
278    const offer = meta.offerName || getOffer() || 'Offer';
279    const payload = {
280      slug,
281      name: company,
282      company,
283      offer,
284      offerSlug: slugify(offer),
285      sourceUrl: meta.url || getUrl(),
286      status: meta.status || 'draft'
287    };
288
289    if (!global.PathCreativesSupabase) return payload;
290    try {
291      await global.PathCreativesSupabase.init();
292      await global.PathCreativesSupabase.upsertProject(payload);
293      if (meta.url || getUrl()) {
294        await recordWizardStep(1, 'url_entered', { url: payload.sourceUrl });
295      }
296    } catch (err) {
297      console.warn('[PathCreativesProject] ensureProjectRow failed', err);
298    }
299    return payload;
300  }
301
302  async function saveBrandKit(scrape) {
303    const kit = brandKitFromScrape(scrape);
304    if (!kit) return false;
305    setScrape(scrape);
306    await ensureProjectRow({
307      companyName: getCompany(),
308      url: scrape.url || getUrl()
309    });
310
311    if (!global.PathCreativesSupabase) return false;
312    try {
313      await global.PathCreativesSupabase.init();
314      return global.PathCreativesSupabase.saveBrandKit(getSlug(), kit);
315    } catch (err) {
316      console.warn('[PathCreativesProject] saveBrandKit failed', err);
317      return false;
318    }
319  }
320
321  async function persistDraftBrief(brief) {
322    const payload = stripBriefForStorage(brief);
323    if (!payload) return false;
324    setDraftBrief(payload);
325    sessionStorage.setItem(KEYS.briefingSeen, '1');
326
327    await ensureProjectRow({
328      companyName: payload.companyName || getCompany(),
329      offerName: payload.offerName || getOffer(),
330      url: payload.url || getUrl()
331    });
332
333    if (!global.PathCreativesSupabase) return false;
334    try {
335      await global.PathCreativesSupabase.init();
336      return global.PathCreativesSupabase.upsertProject({
337        slug: getSlug(),
338        name: payload.companyName || getCompany() || getSlug(),
339        company: payload.companyName || getCompany(),
340        offer: payload.offerName || getOffer(),
341        offerSlug: slugify(payload.offerName || getOffer()),
342        sourceUrl: payload.url || getUrl(),
343        status: 'draft',
344        brief: payload,
345        briefApproved: false,
346        briefSummary: payload.overview || null,
347        briefStrategy: payload.brandContext?.strategy || null
348      });
349    } catch (err) {
350      console.warn('[PathCreativesProject] persistDraftBrief failed', err);
351      return false;
352    }
353  }
354
355  async function persistApprovedBrief(brief) {
356    const payload = stripBriefForStorage(brief);
357    if (!payload) return false;
358    setApprovedBrief(payload);
359    sessionStorage.setItem(KEYS.briefingSeen, '1');
360
361    await ensureProjectRow({
362      companyName: payload.companyName || getCompany(),
363      offerName: payload.offerName || getOffer(),
364      url: payload.url || getUrl(),
365      status: 'brief-approved'
366    });
367
368    if (!global.PathCreativesSupabase) return false;
369    try {
370      await global.PathCreativesSupabase.init();
371      const ok = await global.PathCreativesSupabase.upsertProject({
372        slug: getSlug(),
373        name: payload.companyName || getCompany() || getSlug(),
374        company: payload.companyName || getCompany(),
375        offer: payload.offerName || getOffer(),
376        offerSlug: slugify(payload.offerName || getOffer()),
377        sourceUrl: payload.url || getUrl(),
378        status: 'brief-approved',
379        brief: payload,
380        briefApproved: true,
381        briefSummary: payload.overview || null,
382        briefStrategy: payload.brandContext?.strategy || null
383      });
384      if (ok) await recordWizardStep(1, 'step_approved');
385      return ok;
386    } catch (err) {
387      console.warn('[PathCreativesProject] persistApprovedBrief failed', err);
388      return false;
389    }
390  }
391
392  async function hydrateFromSupabase() {
393    if (!global.PathCreativesSupabase) return null;
394    try {
395      await global.PathCreativesSupabase.init();
396      const slug = getSlug();
397      const [project, design] = await Promise.all([
398        global.PathCreativesSupabase.loadProject(slug),
399        global.PathCreativesSupabase.loadDesign(slug)
400      ]);
401
402      if (project?.sourceUrl) setUrl(project.sourceUrl);
403      if (project?.company) setCompany(project.company);
404      if (project?.offer) setOffer(project.offer);
405      if (project?.slug) setSlug(project.slug);
406
407      if (project?.briefApproved && project.brief) {
408        setApprovedBrief({ ...project.brief, url: project.sourceUrl || project.brief.url });
409        sessionStorage.setItem(KEYS.briefingSeen, '1');
410      } else if (project?.brief) {
411        setDraftBrief({ ...project.brief, url: project.sourceUrl || project.brief.url });
412        sessionStorage.setItem(KEYS.briefingSeen, '1');
413      }
414
415      if (design) {
416        if (design.assetsApproved) markAssetsApprovedSession();
417        if (design.imageryApproved) markImageryApprovedSession();
418
419        if (global.PathCreativesDesign && (design.assetsApproved || design.imageryApproved)) {
420          try {
421            const local = global.PathCreativesDesign.load();
422            if (
423              (design.assetsApproved && !local.assetsApproved)
424              || (design.imageryApproved && !local.imageryApproved)
425            ) {
426              global.PathCreativesDesign.save({
427                ...local,
428                assetsApproved: Boolean(design.assetsApproved || local.assetsApproved),
429                imageryApproved: Boolean(design.imageryApproved || local.imageryApproved)
430              });
431            }
432          } catch (_) {
433            /* ignore */
434          }
435        }
436
437        const localScrape = getScrape() || {};
438        const mergedScrape = {
439          ...localScrape,
440          url: design.scrapeMeta?.url || localScrape.url || getUrl(),
441          title: design.scrapeMeta?.title || localScrape.title || '',
442          description: design.scrapeMeta?.description || localScrape.description || '',
443          manual: design.scrapeMeta?.manual ?? localScrape.manual,
444          scrapedAt: design.scrapeMeta?.scrapedAt || localScrape.scrapedAt,
445          colors: (() => {
446            const sessionColors = getKitColorsSession();
447            if (sessionColors?.length && !global.PathCreativesDesign?.isPlaceholderColors?.(sessionColors)) {
448              return sessionColors;
449            }
450            if (global.PathCreativesDesign?.isPlaceholderColors?.(design.colors) && localScrape.colors?.length) {
451              return localScrape.colors;
452            }
453            return pickAssetArray(design.colors, localScrape.colors);
454          })(),
455          logos: pickAssetArray(design.logos, localScrape.logos),
456          imagery: pickAssetArray(design.pageImagery, localScrape.imagery),
457          screenshot: localScrape.screenshot || null
458        };
459        setScrape(mergedScrape);
460      }
461
462      return { project, design };
463    } catch (err) {
464      console.warn('[PathCreativesProject] hydrateFromSupabase failed', err);
465      return null;
466    }
467  }
468
469  function requireUrl(fallback) {
470    const url = getUrl();
471    if (url) return url;
472    if (fallback) {
473      setUrl(fallback);
474      return fallback;
475    }
476    return '';
477  }
478
479  function clearSlugScopedWizardState(explicitSlug, explicitOffer) {
480    const slug = explicitSlug || getSlug();
481    const offer = explicitOffer || getOffer();
482    try {
483      clearAssetsApprovedSession();
484      clearImageryApprovedSession();
485      sessionStorage.removeItem(`pc:kit-colors:${slug}`);
486      localStorage.removeItem(`pc:design:${slug}`);
487      localStorage.removeItem(`pc:generate:${slug}-${slugify(offer || 'offer')}`);
488    } catch (_) {
489      /* ignore */
490    }
491  }
492
493  function clearAllLocalProjectCaches() {
494    try {
495      const keysToRemove = [];
496      for (let i = 0; i < localStorage.length; i += 1) {
497        const key = localStorage.key(i);
498        if (key && (key.startsWith('pc:design:') || key.startsWith('pc:generate:'))) {
499          keysToRemove.push(key);
500        }
501      }
502      keysToRemove.forEach((key) => localStorage.removeItem(key));
503    } catch (_) {
504      /* ignore */
505    }
506  }
507
508  function clearWizard() {
509    const slug = getSlug();
510    const offer = getOffer();
511    Object.values(KEYS).forEach((key) => sessionStorage.removeItem(key));
512    clearSlugScopedWizardState(slug, offer);
513  }
514
515  function resetProjectForNewAnalysis() {
516    const preservedUrl = getUrl();
517    clearAllLocalProjectCaches();
518    clearWizard();
519    if (preservedUrl) setUrl(preservedUrl);
520    if (global.PathCreativesDesign?.clear) {
521      global.PathCreativesDesign.clear();
522    }
523  }
524
525  function resolveProjectUrl() {
526    const direct = getUrl();
527    if (direct) return direct;
528
529    const scrapeUrl = getScrape()?.url;
530    if (scrapeUrl) {
531      setUrl(scrapeUrl);
532      return scrapeUrl;
533    }
534
535    const approvedUrl = getApprovedBrief()?.url;
536    if (approvedUrl) {
537      setUrl(approvedUrl);
538      return approvedUrl;
539    }
540
541    const draftUrl = getDraftBrief()?.url;
542    if (draftUrl) {
543      setUrl(draftUrl);
544      return draftUrl;
545    }
546
547    return '';
548  }
549
550  function designStorageKey() {
551    return `pc:design:${getSlug()}`;
552  }
553
554  function generateStorageKey() {
555    const slug = getSlug();
556    const offer = slugify(getOffer() || 'offer');
557    return `pc:generate:${slug}-${offer}`;
558  }
559
560  function imageryApprovedSessionKey() {
561    return `pc:imagery-approved:${getSlug()}`;
562  }
563
564  function assetsApprovedSessionKey() {
565    return `pc:assets-approved:${getSlug()}`;
566  }
567
568  function kitColorsSessionKey() {
569    return `pc:kit-colors:${getSlug()}`;
570  }
571
572  function setKitColorsSession(colors) {
573    if (!Array.isArray(colors) || !colors.length) return;
574    try {
575      sessionStorage.setItem(kitColorsSessionKey(), JSON.stringify(colors));
576    } catch (_) {
577      /* ignore */
578    }
579  }
580
581  function getKitColorsSession() {
582    try {
583      const raw = sessionStorage.getItem(kitColorsSessionKey());
584      if (!raw) return null;
585      const parsed = JSON.parse(raw);
586      return Array.isArray(parsed) ? parsed : null;
587    } catch (_) {
588      return null;
589    }
590  }
591
592  function markAssetsApprovedSession() {
593    try {
594      sessionStorage.setItem(KEYS.assetsApprovedGlobal, '1');
595      sessionStorage.setItem(assetsApprovedSessionKey(), '1');
596    } catch (_) {
597      /* ignore */
598    }
599  }
600
601  function readLocalDesignApproval() {
602    try {
603      const raw = localStorage.getItem(designStorageKey());
604      if (!raw) return { assetsApproved: false, imageryApproved: false };
605      const data = JSON.parse(raw);
606      return {
607        assetsApproved: Boolean(data.assetsApproved),
608        imageryApproved: Boolean(data.imageryApproved)
609      };
610    } catch (_) {
611      return { assetsApproved: false, imageryApproved: false };
612    }
613  }
614
615  function isAssetsApprovedSession() {
616    try {
617      return sessionStorage.getItem(KEYS.assetsApprovedGlobal) === '1'
618        || sessionStorage.getItem(assetsApprovedSessionKey()) === '1';
619    } catch (_) {
620      return false;
621    }
622  }
623
624  function isAssetsApproved() {
625    const persisted = readLocalDesignApproval();
626    return isAssetsApprovedSession() || persisted.assetsApproved;
627  }
628
629  function clearAssetsApprovedSession() {
630    try {
631      sessionStorage.removeItem(KEYS.assetsApprovedGlobal);
632      sessionStorage.removeItem(assetsApprovedSessionKey());
633    } catch (_) {
634      /* ignore */
635    }
636  }
637
638  function markImageryApprovedSession() {
639    try {
640      sessionStorage.setItem(KEYS.imageryApprovedGlobal, '1');
641      sessionStorage.setItem(imageryApprovedSessionKey(), '1');
642    } catch (_) {
643      /* ignore */
644    }
645  }
646
647  function isImageryApprovedSession() {
648    try {
649      return sessionStorage.getItem(KEYS.imageryApprovedGlobal) === '1'
650        || sessionStorage.getItem(imageryApprovedSessionKey()) === '1';
651    } catch (_) {
652      return false;
653    }
654  }
655
656  function isImageryApproved() {
657    const persisted = readLocalDesignApproval();
658    return isImageryApprovedSession() || persisted.imageryApproved;
659  }
660
661  function clearImageryApprovedSession() {
662    try {
663      sessionStorage.removeItem(KEYS.imageryApprovedGlobal);
664      sessionStorage.removeItem(imageryApprovedSessionKey());
665    } catch (_) {
666      /* ignore */
667    }
668  }
669
670  function clearGenerateState() {
671    try {
672      localStorage.removeItem(generateStorageKey());
673    } catch (_) {
674      /* ignore */
675    }
676    sessionStorage.removeItem(KEYS.generateSeen);
677    /* Assets / imagery session flags are intentionally kept so re-entering Compile does not bounce. */
678  }
679
680  function applyWizardSessionFlags(state) {
681    const merged = { ...(state || {}) };
682    if (!merged.assetsApproved && isAssetsApproved()) {
683      merged.assetsApproved = true;
684    }
685    if (!merged.imageryApproved && isImageryApproved()) {
686      merged.imageryApproved = true;
687    }
688    const sessionColors = getKitColorsSession();
689    if (
690      sessionColors?.length
691      && global.PathCreativesDesign?.isPlaceholderColors?.(merged.colors)
692    ) {
693      merged.colors = sessionColors;
694    }
695    return merged;
696  }
697
698  function canAccessAssetsStep() {
699    const brief = getApprovedBrief();
700    return Boolean(isBriefApproved() && brief && getBriefAds(brief).length);
701  }
702
703  function canAccessDesignStep() {
704    return Boolean(canAccessAssetsStep() && isAssetsApproved());
705  }
706
707  function canAccessGenerateStep() {
708    return Boolean(canAccessDesignStep() && isImageryApproved());
709  }
710
711  function getBriefAds(brief) {
712    if (!brief) return [];
713    if (Array.isArray(brief.ads) && brief.ads.length) return brief.ads;
714    if (brief.option?.ads?.length) return brief.option.ads;
715    return [];
716  }
717
718  function getBriefOverview(brief) {
719    if (!brief) return '';
720    return brief.overview || brief.option?.overview || '';
721  }
722
723  global.PathCreativesProject = {
724    KEYS,
725    slugify,
726    getUrl,
727    setUrl,
728    resolveProjectUrl,
729    getScrape,
730    setScrape,
731    scrapeMetaFromScrape,
732    brandKitFromScrape,
733    getCompany,
734    setCompany,
735    getOffer,
736    setOffer,
737    getSlug,
738    setSlug,
739    getDraftBrief,
740    setDraftBrief,
741    isBriefApproved,
742    getApprovedBrief,
743    setApprovedBrief,
744    getBriefAds,
745    canAccessAssetsStep,
746    canAccessDesignStep,
747    canAccessGenerateStep,
748    getBriefOverview,
749    syncMetaFromBrief,
750    stripBriefForStorage,
751    ensureProjectRow,
752    saveBrandKit,
753    persistDraftBrief,
754    persistApprovedBrief,
755    hydrateFromSupabase,
756    requireUrl,
757    clearWizard,
758    clearAllLocalProjectCaches,
759    resetProjectForNewAnalysis,
760    clearSlugScopedWizardState,
761    designStorageKey,
762    generateStorageKey,
763    clearGenerateState,
764    applyWizardSessionFlags,
765    readLocalDesignApproval,
766    markAssetsApprovedSession,
767    isAssetsApprovedSession,
768    isAssetsApproved,
769    clearAssetsApprovedSession,
770    setKitColorsSession,
771    getKitColorsSession,
772    markImageryApprovedSession,
773    isImageryApprovedSession,
774    isImageryApproved,
775    clearImageryApprovedSession,
776    getProjectMode,
777    setProjectMode,
778    isDemoMode,
779    getFlowMode,
780    setFlowMode,
781    isConsolidatedFlow,
782    recordWizardStep,
783    saveProjectHistory
784  };
785})(typeof window !== 'undefined' ? window : globalThis);

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.