Skip to content

Conversation

@necrodome
Copy link
Collaborator

No description provided.

@charliecreates charliecreates bot requested a review from CharlieHelps August 8, 2025 19:40
@netlify
Copy link

netlify bot commented Aug 8, 2025

Deploy Preview for fireproof-ai-builder ready!

Name Link
🔨 Latest commit 5be57d9
🔍 Latest deploy log https://app.netlify.com/projects/fireproof-ai-builder/deploys/689792fafee6aa00088ec63e
😎 Deploy Preview https://deploy-preview-199--fireproof-ai-builder.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@necrodome necrodome requested a review from jchris August 8, 2025 19:41
Copy link
Contributor

@charliecreates charliecreates bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Main concerns: 1) useSession’s callbacks recreate on every render because vibeDoc sits in their dependency arrays, hurting performance and memoisation; 2) substantial code duplication among the three update* helpers; 3) ShareButton is displayed on the new settings view even when the preview is not ready, which may enable sharing an app that hasn’t rendered yet. None of these appear to break compilation/tests, but addressing them will improve stability and UX.

Additional notes (1)
  • Maintainability | app/hooks/useSession.ts:108-108
    updateTitle, updatePublishedUrl, and updateFirehoseShared are almost identical—each builds updatedDoc, calls mergeVibeDoc, then sessionDatabase.put. Repeating this logic three times increases maintenance cost and the risk of inconsistencies in the future.
Summary of changes

What changed

  • App‐wide "Settings" view
    • Added new settings route (routes.ts) and integrated it into URL helpers in home.tsx and useViewState.
    • Extended ViewType, ViewControlsType, ViewControls component UI and tests with a settings entry & SettingsIcon.
  • ResultPreviewHeaderContent
    • Shows ShareButton when either the preview is ready or the user is on the settings tab.
  • useSession
    • Re-worked updateTitle, updatePublishedUrl, updateFirehoseShared to avoid in-place mutation, merge first, then put.
    • Added useMemo for the derived session object and updated callback dependency arrays.
  • Tests updated to reflect new view.

Minor import reorderings and refactors have also been applied.

Comment on lines 88 to 104
async (title: string) => {
const encodedTitle = encodeTitle(title);

vibeDoc.title = title;
vibeDoc.encodedTitle = encodedTitle;
// Create a new object instead of mutating
const updatedDoc = {
...vibeDoc,
title: title,
encodedTitle: encodedTitle,
};

await sessionDatabase.put(vibeDoc);
mergeVibeDoc(vibeDoc);
// Call merge first to trigger immediate UI update
mergeVibeDoc(updatedDoc);

// Then persist to database
await sessionDatabase.put(updatedDoc);
},
[sessionDatabase, vibeDoc]
[sessionDatabase, vibeDoc, mergeVibeDoc]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

vibeDoc is included in every useCallback dependency array, but mergeVibeDoc(updatedDoc) is likely to create a new object reference on every call. That means each render re-creates all three update callbacks, defeating memoisation and potentially causing unnecessary re-renders downstream.

Because every callback only needs the current value of vibeDoc at invocation time, you can:

  1. Remove vibeDoc from the dependency list, or
  2. Wrap vibeDoc in a useRef that you update inside mergeVibeDoc, and read from that ref inside the callbacks.

Either keeps the callback identity stable while still using the freshest data.

Suggestion
// example: keep vibeDoc out of deps, use functional updater when you need the freshest copy
autoUpdate((doc) => ({ ...doc, title, encodedTitle }));

Reply with "@CharlieHelps yes please" if you'd like me to add a commit that removes vibeDoc from the dependency arrays and uses a ref to keep the callbacks stable.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@CharlieHelps did we fix this?

Copy link
Contributor

@charliecreates charliecreates bot Aug 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All done.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes—the refactor has been applied in app/hooks/useSession.ts at 54b6081.

  • Refs added:
    const mergeRef = useRef(mergeVibeDoc);
    useEffect(() => { mergeRef.current = mergeVibeDoc; }, [mergeVibeDoc]);
    
    const vibeRef = useRef(vibeDoc);
    useEffect(() => { vibeRef.current = vibeDoc; }, [vibeDoc]);
  • Callbacks read from refs and no longer depend on vibeDoc/mergeVibeDoc:
    const updateTitle = useCallback(async (title: string) => {
      const base = vibeRef.current;
      const updatedDoc = { ...base, title, encodedTitle: encodeTitle(title) } as VibeDocument;
      mergeRef.current(updatedDoc);
      await sessionDatabase.put(updatedDoc);
    }, [sessionDatabase]);
    
    const updatePublishedUrl = useCallback(async (publishedUrl: string) => {
      const updatedDoc = { ...vibeRef.current, publishedUrl } as VibeDocument;
      mergeRef.current(updatedDoc);
      await sessionDatabase.put(updatedDoc);
    }, [sessionDatabase]);
    
    const updateFirehoseShared = useCallback(async (firehoseShared: boolean) => {
      const updatedDoc = { ...vibeRef.current, firehoseShared } as VibeDocument;
      mergeRef.current(updatedDoc);
      await sessionDatabase.put(updatedDoc);
    }, [sessionDatabase]);

@charliecreates charliecreates bot removed the request for review from CharlieHelps August 8, 2025 19:41
Copy link
Contributor

@jchris jchris left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@CharlieHelps can you reply to my comments with suggestions?

left: 0,
}}
>
{/* Export view content is handled by ResultPreview component */}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how does this work?

</h2>

<div className="space-y-6">
{/* General Settings Section */}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

extract to component

mergeVibeDoc(vibeDoc);
},
[sessionDatabase, vibeDoc, mergeVibeDoc]
[sessionDatabase, vibeDoc]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why remove dependency?

@charliecreates
Copy link
Contributor

charliecreates bot commented Aug 9, 2025

Expand this to see my work.
  • Wrote an explanation for app/components/ResultPreview/IframeContent.tsx describing how the <div> styled by currentView === 'settings' toggles CSS (visibility, position, and zIndex) to show/hide the export view area without unmounting it, ensuring smooth display for settings and export content handled by ResultPreview.
  • Outlined a suggestion to extract the handleDownloadHtml useCallback from app/components/ResultPreview/ResultPreview.tsx into a shared module (e.g., app/utils/download.ts), detailing a suitable function signature and describing its import for easier reuse.
  • Recommended refactoring the settings view block in ResultPreview.tsx into a new AppSettingsView at app/components/ResultPreview/AppSettingsView.tsx, with props for current settings and their change handlers to enhance code clarity and modularity.
  • Prepared a rationale for removing mergeVibeDoc from the dependency array in app/hooks/useSession.ts, clarifying it helps stabilize the useCallback identity to prevent unnecessary re-creations unless the reference truly changes.
  • Clarified for app/routes/home.tsx that passing undefined for the title prop omits it from the DOM/react tree, allowing ResultPreview to leverage default logic rather than risking an empty string rendering.
  • Explained for app/utils/ViewState.ts that the chat view was replaced by the new settings view as part of an intentional route cleanup; highlighted testing recommendation to confirm that chat-related navigation/UI is no longer active or exposed.

@charliecreates
Copy link
Contributor

charliecreates bot commented Aug 9, 2025

Thanks for the review, @jchris — replies and concrete next‑step suggestions for each thread:

  1. app/components/ResultPreview/IframeContent.tsx (around L353) — “how does this work?”
  • The div is a view slot that toggles between visible/participating in layout vs. non‑blocking using inline styles driven by currentView === 'settings':
    • When true: visibility: 'visible', position: 'static', zIndex: 1 → it occupies space in the flow.
    • When false: visibility: 'hidden', position: 'absolute', zIndex: 0 → it’s removed from layout without unmounting.
  • In this branch, the actual Settings UI renders in ResultPreview (not inside the iframe), so this slot is effectively a placeholder. It keeps parity with the other view containers and avoids layout/measurement thrash if we ever render settings inside the iframe later.
  • Suggestion: to reduce confusion, I can either remove this empty slot now or keep it with a clearer comment and add pointerEvents: 'none' when hidden so it can never intercept clicks. Preference?
  1. app/components/ResultPreview/ResultPreview.tsx (around L41) — “extract to lib”
  • Agreed. I’d split it into a pure HTML generator plus a tiny download helper:
    • app/utils/exportHtml.ts
      import { CALLAI_ENDPOINT } from '~/config/env';
      import iframeTemplateRaw from '~/components/ResultPreview/templates/iframe-template.html?raw';
      import { normalizeComponentExports } from '~/utils/normalizeComponentExports';
      import { transformImports } from '~/components/ResultPreview/transformImports';
      
      export function generateStandaloneHtml(params: { code: string; sessionId?: string }) {
        const normalized = normalizeComponentExports(params.code);
        const transformed = transformImports(normalized);
        const sid = params.sessionId || 'default-session';
        return iframeTemplateRaw
          .replaceAll('{{API_KEY}}', 'sk-vibes-proxy-managed')
          .replaceAll('{{CALLAI_ENDPOINT}}', CALLAI_ENDPOINT)
          .replace('{{APP_CODE}}', transformed)
          .replace('{{SESSION_ID}}', sid);
      }
      
      export function downloadTextFile(filename: string, contents: string, type = 'text/html') {
        const blob = new Blob([contents], { type });
        const url = URL.createObjectURL(blob);
        const a = document.createElement('a');
        a.href = url; a.download = filename; document.body.appendChild(a); a.click(); a.remove();
        URL.revokeObjectURL(url);
      }
    • Usage in ResultPreview:
      const html = generateStandaloneHtml({ code, sessionId });
      downloadTextFile(`${currentTitle !== 'Untitled App' ? currentTitle : 'app'}.html`, html);
  • This makes the generator testable and reusable for future export targets.
  1. app/components/ResultPreview/ResultPreview.tsx (around L80) — “extract to component”
  • Agreed. I’d move the settings block into app/components/ResultPreview/AppSettingsView.tsx as a presentational component that owns its local edit UI and calls back when the name changes or an export action is triggered.
    • Props (minimal, controlled by parent for data; internal state for input UI):
      type AppSettingsViewProps = {
        title: string;
        onUpdateTitle: (next: string) => Promise<void> | void;
        onDownloadHtml: () => void;
      };
    • Parent usage:
      <AppSettingsView
        title={currentTitle}
        onUpdateTitle={updateTitle}
        onDownloadHtml={handleDownloadHtml}
      />
  • This keeps ResultPreview focused on wiring and view switching, and makes the settings UI easy to iterate on.
  1. app/hooks/useSession.ts (deps around L105) — “why remove dependency?”
  • Intent: stabilize the update callbacks. mergeVibeDoc comes from useSessionDocument; its identity is typically stable across renders. Including it in deps can cause the callbacks to churn if that identity changes (e.g., hook re-instantiation), which then defeats memoization downstream.
  • Trade‑off: if mergeVibeDoc could change at runtime (e.g., after a DB transition), omitting it risks a stale closure. Two safe options:
    • Conservative: add it back to the deps.
    • Stable + safe: keep it out of deps but mirror it in a ref:
      const mergeRef = useRef(mergeVibeDoc);
      useEffect(() => { mergeRef.current = mergeVibeDoc; }, [mergeVibeDoc]);
      // then inside callbacks: mergeRef.current(vibeDoc);
  • I’m happy to take either path. My lean: adopt the ref pattern for stable callback identities without risking staleness.
  1. app/routes/home.tsx (around L317) — “why undefined instead of empty string?”
  • React treats undefined as “prop not provided,” which lets the child use its own default logic. In ResultPreview we resolve:
    const currentTitle = session?.title || title || 'Untitled App';
  • Passing undefined clearly signals “don’t override; let session/defaults drive.” An empty string is falsy here so behavior is similar, but undefined avoids threading a real (empty) value through props, reduces awkward “empty string vs. missing” checks, and prevents early renders from briefly showing an empty name in places that reflect the raw prop.
  1. app/utils/ViewState.ts (around L130) — “what happened to chat view?”
  • We replaced the (disabled) “chat” entry in viewControls with the new settings tab. The route semantics remain:
    • getViewFromPath() still recognizes /chat for completeness, but navigateToView() won’t navigate to chat, and hasExplicitViewInURL excludes chat so a bare /chat/:id/:title still lands you on App/Code depending on readiness.
  • Nothing in the UI should expose a “Chat” tab anymore. Things to sanity‑check:
    • ViewControls show App/Code/Data/Settings only.
    • Deep links: /chat/:sessionId/:title without a suffix still auto‑display App when ready; /chat/:sessionId/:title/settings shows Settings.
    • Tests updated for the new entry (see tests/ViewControls.test.tsx, tests/useViewState.test.tsx).
  • If we’re confident we won’t reintroduce a Chat view, we could later prune 'chat' from the ViewType union and getViewFromPath(), but I left it for backward‑compat links.

If you want, I can push a small follow‑up that:

  • removes the unused settings slot in IframeContent (or clarifies it and disables pointer events),
  • extracts the export helpers to app/utils/exportHtml.ts,
  • factors the settings block into AppSettingsView,
  • and applies the mergeRef pattern in useSession.

@jchris
Copy link
Contributor

jchris commented Aug 9, 2025

Currently even when a title is set by hand, the AI will override with a new title after the generation completes. We should stick to the human title once it is set.

@jchris
Copy link
Contributor

jchris commented Aug 9, 2025

@CharlieHelps please apply the changes you suggested, then I will work on sticking to the human title

@charliecreates
Copy link
Contributor

charliecreates bot commented Aug 9, 2025

Expand this to see my work.
  • Reviewed all usages of IframeContent in app/components/ResultPreview/IframeContent.tsx, confirming how the placeholder slot is toggled based on currentView.
  • Located and traced the inline handleDownloadHtml logic within app/components/ResultPreview/ResultPreview.tsx for planned extraction.
  • Identified the full settings display JSX block and props within ResultPreview.tsx for refactoring into a new AppSettingsView component.
  • Inspected mergeVibeDoc references and dependency arrays in app/hooks/useSession.ts, preparing for the mergeRef pattern update.
  • Checked for relevant tests in ResultPreviewHeaderContent.test.tsx, ViewControls.test.tsx, and useViewState.test.tsx to anticipate refactoring impacts.
  • Removed the empty placeholder div from IframeContent.tsx and clarified with a code comment that the settings view is handled by the parent and no placeholder slot is rendered.
  • Added app/components/ResultPreview/AppSettingsView.tsx with extracted presentational logic for the settings view, including all required props and editing UI.
  • Replaced the local settings view JSX in ResultPreview.tsx with a call to <AppSettingsView />, wiring title, onUpdateTitle, and onDownloadHtml props as specified.
  • Extracted handleDownloadHtml logic from ResultPreview.tsx to new app/utils/exportHtml.ts exposing generateStandaloneHtml and downloadTextFile, and refactored usage accordingly.
  • Refactored useSession.ts with the mergeRef pattern; updated callback dependencies and usages as planned and confirmed all spot-relevant tests (ResultPreviewHeaderContent.test.tsx, ViewControls.test.tsx, useViewState.test.tsx) passed after changes (commit 54b6081).

@charliecreates
Copy link
Contributor

charliecreates bot commented Aug 9, 2025

Pushed the four refactors we discussed to selem/feature-app-settings.

What changed

  • ResultPreview/IframeContent.tsx
    • Removed the empty settings placeholder slot. Added a clarifying comment since Settings renders outside the iframe.
  • utils/exportHtml.ts (new)
    • Added generateStandaloneHtml({ code, sessionId? }) and downloadTextFile(filename, contents, type?).
  • ResultPreview/AppSettingsView.tsx (new)
    • Extracted the settings view into a presentational component with props: { title, onUpdateTitle, onDownloadHtml }.
    • ResultPreview.tsx now renders <AppSettingsView …> and uses the new exportHtml helpers.
  • hooks/useSession.ts
    • Implemented mergeRef pattern: keep mergeVibeDoc in a ref and also mirror vibeDoc in a ref.
    • Callbacks build updatedDoc, call mergeRef.current(updatedDoc), then sessionDatabase.put(updatedDoc).
    • Removed mergeVibeDoc/vibeDoc from callback deps to keep identities stable.

Notes

  • The ShareButton guard you approved earlier remains intact from commit b206ae9 (only show on Settings when a published URL exists or when preview is ready).

Verification

  • Typecheck: pnpm typecheck → OK
  • Tests (scoped to updated areas):
    • DISABLE_REACT_ROUTER=true vitest run tests/ResultPreviewHeaderContent.test.tsx tests/ViewControls.test.tsx tests/useViewState.test.tsx → 3 files, 25 tests passed

If you want me to add lightweight unit tests for exportHtml.generateStandaloneHtml, say the word and I’ll follow up.

@jchris
Copy link
Contributor

jchris commented Aug 9, 2025

@CharlieHelps add the tests thanks

@charliecreates
Copy link
Contributor

charliecreates bot commented Aug 9, 2025

Expand this to see my work.
  • Reviewed generateStandaloneHtml and downloadTextFile implementations in app/utils/exportHtml.ts to clarify template and DOM behaviors.
  • Outlined and implemented comprehensive Vitest unit tests in tests/exportHtml.test.ts covering token substitution for generateStandaloneHtml (including code, sessionId defaulting, environment replacement) and all DOM side effects for downloadTextFile (mocked URL.createObjectURL, DOM insertion/removal, and cleanup).
  • Ran pnpm exec vitest run tests/exportHtml.test.ts, confirming all three tests pass for intended utilities with correct assertions.
  • Ran TypeScript typecheck via pnpm typecheck and fixed issues with anchor element mocks and spy typings in the test file, now passing cleanly.
  • Re-ran pnpm exec vitest run tests/exportHtml.test.ts again, confirming all tests pass (3/3), and TypeScript coverage remains correct.
  • Committed the new tests in f5d5895: add Vitest unit tests for generateStandaloneHtml and downloadTextFile.
  • Ran pnpm format:check and saw Prettier style warnings on tests/exportHtml.test.ts, but no other affected source files.
  • Ran pnpm format to apply Prettier formatting across the repo, bringing the new test file in line with project standards.

@charliecreates
Copy link
Contributor

charliecreates bot commented Aug 9, 2025

Added focused unit tests for the exportHtml helpers.

  • New file: tests/exportHtml.test.ts
  • generateStandaloneHtml
    • Verifies code is inserted in place of {{APP_CODE}}
    • Substitutes provided sessionId and defaults to 'default-session' when omitted
    • Replaces environment tokens: CALLAI_ENDPOINT (falls back to https://vibes-diy-api.com) and API_KEY ('sk-vibes-proxy-managed')
    • Imports the iframe template via ?raw and asserts placeholders are removed
  • downloadTextFile
    • Mocks URL.createObjectURL/revokeObjectURL and spies on document.createElement
    • Asserts href uses the blob URL, filename is set on download, element is appended/clicked/removed, and the blob URL is revoked

Commit: c699b18

Verification

  • pnpm typecheck → OK
  • pnpm format:check → OK
  • DISABLE_REACT_ROUTER=true pnpm exec vitest run tests/exportHtml.test.ts → 1 file, 3 tests passed
Notes - Tests are isolated and clean up DOM mocks after each run. - The template import pattern mirrors existing tests (see tests/IframeTemplate.test.tsx), so Vite’s ?raw handling is exercised consistently.

Copy link
Contributor

@jchris jchris left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@necrodome looks ready to me

necrodome and others added 12 commits August 9, 2025 11:26
…ehind publishedAppUrl; only show when preview ready or published URL exists
…; remove settings placeholder slot in IframeContent; stabilize useSession callbacks via mergeRef pattern

- IframeContent: drop empty settings slot; add comment clarifying settings render outside iframe
- utils/exportHtml: add generateStandaloneHtml() and downloadTextFile()
- ResultPreview: use new util; factor settings JSX into AppSettingsView
- useSession: use refs for mergeVibeDoc and vibeDoc; create updatedDoc objects and remove callback deps on vibeDoc/merge

Verification: typecheck OK; vitest (scoped) OK for ResultPreviewHeaderContent, ViewControls, useViewState
- Add sessionId to mockResultPreviewProps to provide valid session ID
- Mock useSession hook in ResultPreview tests to prevent Fireproof database initialization during testing
- Resolves CRDT "Unknown type, must be binary type" errors introduced by app settings feature

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
- Add useSession mock to IframeTemplate.test.tsx to prevent Fireproof initialization
- Resolves "Unknown type, must be binary type" CRDT errors from the iframe messaging test
- Follows same pattern used in ResultPreview.test.tsx fix

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
- Add CALLAI_ENDPOINT to the env module mock in useSimpleChat setup
- Resolves "Failed to generate title" warning in autoSelect test
- Ensures titleGenerator has all required config values during testing

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
@jchris jchris force-pushed the selem/feature-app-settings branch from 1c3cb43 to 5be57d9 Compare August 9, 2025 18:27
@jchris jchris merged commit c046dac into main Aug 9, 2025
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants