You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Folders for the Recent Generations gallery. Tiles can now be grouped into named folders. The gallery shows a folder tab strip above the grid (each chip displays a count and is filterable), every user gets an auto-seeded Inbox (renameable, undeletable), and a "New folder" chip on the strip starts an inline creation flow. Each folder chip exposes a pin toggle that controls which folder is the sticky default for new generations — with no pin, new tiles fall back to Inbox. The strip's kebab menu offers Rename and Delete (Inbox hides Delete); deleting a folder sweeps every tile inside it into Inbox before removing the folder so nothing is ever stranded. The selection-mode toolbar gets a "Move to folder" picker for bulk reassignment. Folders persist on the user document under preferences.folders / preferences.activeFolderId for signed-in users and in localStorage for guests, so the gallery looks the same before and after sign-in. Every Generation carries a folderId; legacy items without one are normalized into Inbox at read time. New services/folderService.ts, Folder interface, and INBOX_FOLDER_ID constant in types.ts. historyService.moveGenerationsToFolder is the durable bulk-write path used by both selection-mode moves and folder deletion (App.tsx, components/RecentGenerations.tsx, services/folderService.ts, services/historyService.ts, services/authService.ts, types.ts).
Toolbar presets. Save the current toolbar configuration — graphic type, visual style, color scheme, aspect ratio, SVG mode, model, OpenAI image quality — as a named preset and recall it from the new Bookmark dropdown in ControlPanel. Save uses an inline "name your preset" field rather than a modal, with a duplicate-name guard and a default suggested name (Preset N+1) so the common case is zero-friction. Applying a preset uses partial-snapshot semantics: only the fields the preset actually carries are overridden, so users can save narrow snapshots (e.g. "just my favorite style + palette pair") that compose with the rest of the toolbar. Aspect ratios are coerced through getSafeAspectRatioForModel when a preset switches models, so an Instagram-square preset never strands a Gemini-only model on an unsupported ratio. Presets persist on the user document under preferences.presets. Guests don't see the save affordance (presets are signed-in-only); the dropdown still lists existing presets so a one-time guest can apply something the user saved earlier. New services/presetService.ts and ToolbarPreset interface in types.ts (App.tsx, components/ControlPanel.tsx, services/presetService.ts, services/authService.ts, types.ts).
Per-user stats drill-down. New statsService.getUserStats(uid) builds a focused single-user snapshot (profile card, total tiles / images / refinements, daily timeline for the last 30 days, top buckets by model / graphic type / visual style / aspect ratio) by scanning only that user's users/{uid}/history subcollection — cheap regardless of project size, gated behind the same admin-only Firestore rule as the dashboard. The StatsPage adds a FocusedUserView that mirrors the global dashboard's layout, plus a back arrow to return to the all-users view. Avatars use the existing CachedImage component so profile photos survive VPN blocks (components/StatsPage.tsx, services/statsService.ts).
Multi-line prompt input that grows on focus. The prompt field is now a <textarea> with min-h-[48px] focus:h-32 resize-y, so longer prompts no longer scroll horizontally inside a single-row input. Enter still submits; Shift+Enter inserts a newline. The keyboard shortcuts that ignore inputs/textareas/contenteditables already cover this so navigation arrows still won't fire mid-typing (components/ControlPanel.tsx).
Changed
Admins keep an unbounded history.historyService.saveToRemote, historyService.updateRemote, historyService.mergeLocalToRemote, and historyService.updateGeneration now thread an isAdmin flag through to the eviction sweep. Admin accounts skip the REMOTE_LIMIT cull entirely and use a wider REMOTE_SCAN_LIMIT (≥500) when looking up existing tiles, so legacy / audit assets are never evicted just because an admin blew past the regular cap. Regular accounts continue to enforce the existing limit unchanged. App.tsx passes user.isAdmin === true everywhere it triggers a remote write (services/historyService.ts, App.tsx).
Fixed
Toolbar no longer wraps to a second row at desktop widths. The new Presets dropdown nudged the toolbar past its desktop-width budget, leaving the Reset / Upload actions stranded on a second row at lg+ (and visually disconnecting them from the model row). The toolbar container is now flex flex-wrap lg:flex-nowrap … w-full lg:min-w-0, and DropdownButton is shrink-0 lg:shrink lg:min-w-0. At desktop widths the toolbar forces a single row and dropdown labels truncate gracefully when space is tight; below lg we keep flex-wrap so the strip wraps to multiple rows instead of producing a horizontal scrollbar that would clip dropdown menus (components/ControlPanel.tsx).
Admin row-actions kebab no longer disappears under the next row's sticky column. The right-most actions column on the admin user table uses a sticky right-0 background so the kebab stays visible while the table scrolls horizontally, but the kebab popover painted under the next row's sticky background because the cell had no z-context. The cell now lifts to z-30 while its menu is open so dropdown items remain readable across rows (components/AdminPage.tsx).
"Build Brand Analysis" no longer fails behind a VPN. Post-0.6.0 the saved version usually has imageData='' because raster bytes live in Firebase Storage. When the network can't reach firebasestorage.googleapis.com (VPN, captive portal, transient DNS), the analysis service's plain fetch(imageUrl) threw "Failed to fetch" and the action looked broken. App.handleBrandAnalysis now consults the IndexedDB image cache (populated at upload time) as a fallback before sending the image to the analyzer; the original network fetch remains as a last resort (App.tsx).
Generation thumbnails survive VPN / proxy blocks. When firebasestorage.googleapis.com is blocked but Firestore docs come through, generation tiles and the main viewer used to fall back to a broken-image icon because imageData was stripped at upload time. New IndexedDB image cache (services/imageCache.ts) records every uploaded blob keyed on ${generationId}|${versionId}, evicts entries when generations are deleted or aged out of remote history, and a background backfill walks recent history (with concurrency control) to seed the cache after the user has at least one healthy session. RecentGenerations and ImageDisplay now consult the cache from their onError handlers and present a blob: URL instead of failing. URL.revokeObjectURL is wired to component cleanup so the cache doesn't leak object URLs (services/imageCache.ts, services/historyService.ts, services/imageService.ts, components/ImageDisplay.tsx, components/RecentGenerations.tsx).
Profile photos survive VPN / proxy blocks. Same root cause as thumbnails: when firebasestorage.googleapis.com is blocked the user's saved avatar fell back to a broken-image icon. Profile photos now live in the IndexedDB image cache under a reserved __profile__|userId namespace (the prune step skips reserved keys so history-driven eviction never touches profile photos). New reusable <CachedImage> component is the drop-in replacement for <img> and is now used by the header avatar, settings preview, and the admin / stats top-users tables. imageService.uploadProfileImage seeds the cache from the upload buffer immediately so there's no redundant network fetch after a fresh upload (services/imageCache.ts, services/imageService.ts, components/CachedImage.tsx, App.tsx, components/SettingsPage.tsx, components/StatsPage.tsx).