diff --git a/aiui/packages/app/src/composables/contentExtraction.ts b/aiui/packages/app/src/composables/contentExtraction.ts index 638fc783..ed483124 100644 --- a/aiui/packages/app/src/composables/contentExtraction.ts +++ b/aiui/packages/app/src/composables/contentExtraction.ts @@ -4,6 +4,14 @@ import type { MagazineSection } from './contentFiltering' import { mockFilms } from '@/mocks/films' import { mockSongs } from '@/mocks/songs' import { mockPodcasts } from '@/mocks/podcasts' + +// Demo-site content pack (operator decision 2026-08-07): [[film:ID]]-style +// library references resolve against the mock libraries in demo/dev builds +// only. In production the tables constant-fold to empty — no mock module +// reaches the shipped bundle — and ext tags (self-contained) keep working. +const libraryFilms: Film[] = (import.meta.env.DEV || import.meta.env.VITE_DEMO_CONTENT === 'true') ? mockFilms : [] +const librarySongs: Song[] = (import.meta.env.DEV || import.meta.env.VITE_DEMO_CONTENT === 'true') ? mockSongs : [] +const libraryPodcasts: Podcast[] = (import.meta.env.DEV || import.meta.env.VITE_DEMO_CONTENT === 'true') ? mockPodcasts : [] import { isNewsLikeResponse, isMusicQuery, isTVQuery, isBookQuery, isBookLikeResponse, isImageQuery, isPlaceQuery, isPlaceLikeResponse } from './contentFiltering' // ─── Tag regexes ────────────────────────────────────────────────── @@ -437,7 +445,7 @@ export function extractFilmIds(text: string): string[] { function resolveFilms(ids: string[]): Film[] { return ids - .map((id) => mockFilms.find((f) => f.id === id)) + .map((id) => libraryFilms.find((f) => f.id === id)) .filter((f): f is Film => !!f) } @@ -495,7 +503,7 @@ export function extractSongIds(text: string): string[] { function resolveSongs(ids: string[]): Song[] { return ids - .map((id) => mockSongs.find((s) => s.id === id)) + .map((id) => librarySongs.find((s) => s.id === id)) .filter((s): s is Song => !!s) } @@ -560,7 +568,7 @@ function extractSongsFromLibraryMatch(text: string): Song[] { const lower = text.toLowerCase() const found: { song: Song; pos: number }[] = [] const seen = new Set() - for (const song of mockSongs) { + for (const song of librarySongs) { const key = song.id if (seen.has(key)) continue const title = song.title.toLowerCase() @@ -674,7 +682,7 @@ export function extractPodcastIds(text: string): string[] { function resolvePodcasts(ids: string[]): Podcast[] { return ids - .map((id) => mockPodcasts.find((p) => p.id === id)) + .map((id) => libraryPodcasts.find((p) => p.id === id)) .filter((p): p is Podcast => !!p) } diff --git a/aiui/packages/app/src/composables/useAI.ts b/aiui/packages/app/src/composables/useAI.ts index 02b17052..9bf68ba7 100644 --- a/aiui/packages/app/src/composables/useAI.ts +++ b/aiui/packages/app/src/composables/useAI.ts @@ -24,17 +24,25 @@ import { mockFilms } from '@/mocks/films' import { mockSongs } from '@/mocks/songs' import { mockPodcasts } from '@/mocks/podcasts' -const filmContext = mockFilms.map((f) => +// Demo-site content pack (operator decision 2026-08-07): mock libraries are +// presented as "the user's library" ONLY in demo/dev builds. In production +// these constant-fold to empty strings, the prompt's library block vanishes, +// and the mock modules drop out of the bundle entirely. +const filmContext = (import.meta.env.DEV || import.meta.env.VITE_DEMO_CONTENT === 'true') ? mockFilms.map((f) => `- [${f.id}] "${f.title}" (${f.year}) dir. ${f.director} | ${f.genres.join(', ')} | ${f.rating}/10 | On: ${f.sources.map(s => s.type).join(', ')}` -).join('\n') +).join('\n') : '' -const songContext = mockSongs.map((s) => +const songContext = (import.meta.env.DEV || import.meta.env.VITE_DEMO_CONTENT === 'true') ? mockSongs.map((s) => `- [${s.id}] "${s.title}" by ${s.artist}${s.album ? ` (${s.album})` : ''}${s.year ? ` (${s.year})` : ''} | ${(s.genres ?? []).join(', ')} | On: ${(s.sources ?? []).map(x => x.type).join(', ')}` -).join('\n') +).join('\n') : '' -const podcastContext = mockPodcasts.map((p) => +const podcastContext = (import.meta.env.DEV || import.meta.env.VITE_DEMO_CONTENT === 'true') ? mockPodcasts.map((p) => `- [${p.id}] "${p.title}" by ${p.host ?? 'Unknown'}${p.year ? ` (${p.year})` : ''} | ${(p.genres ?? []).join(', ')} | On: ${p.sources.map(x => x.type).join(', ')}` -).join('\n') +).join('\n') : '' + +const librarySection = (import.meta.env.DEV || import.meta.env.VITE_DEMO_CONTENT === 'true') + ? `\nThe user's film library:\n${filmContext}\n\nThe user's song library:\n${songContext}\n\nThe user's podcast library:\n${podcastContext}` + : '' // ─── Wavlake catalog context (fetched at runtime) ──────────── interface WavlakeCatalogTrack { @@ -108,15 +116,7 @@ Prioritize Podcasting 2.0–friendly platforms: Fountain.fm, Podcast Index, Cast **Music discovery:** All music plays from **Wavlake** — a Lightning-powered, Nostr-native music platform. When recommending songs, prefer tracks from the Wavlake trending list (provided below) since those are confirmed playable. For genre requests, use [[song_ext:Title|Artist]] tags — the UI will search Wavlake automatically. Songs not on Wavlake won't play, so stick to Wavlake artists when you can. The user can zap (tip) artists with Lightning directly through the platform. Always include these tags so the UI can render rich cards. Write a brief reason why each is worth checking out. - -The user's film library: -${filmContext} - -The user's song library: -${songContext} - -The user's podcast library: -${podcastContext}` +${librarySection}` const activeProvider = ref('claude') diff --git a/aiui/packages/app/src/composables/useContentPanel.ts b/aiui/packages/app/src/composables/useContentPanel.ts index 24715b31..04c29f41 100644 --- a/aiui/packages/app/src/composables/useContentPanel.ts +++ b/aiui/packages/app/src/composables/useContentPanel.ts @@ -5,6 +5,12 @@ import type { WebSearchResult } from '@aiui/core/types/message' import { mockFilms } from '@/mocks/films' import { mockSongs } from '@/mocks/songs' import { mockPodcasts } from '@/mocks/podcasts' + +// Demo-site content pack (operator decision 2026-08-07): the library views +// fill from mocks in demo/dev builds only; production folds these to empty. +const demoFilms: Film[] = (import.meta.env.DEV || import.meta.env.VITE_DEMO_CONTENT === 'true') ? mockFilms : [] +const demoSongs: Song[] = (import.meta.env.DEV || import.meta.env.VITE_DEMO_CONTENT === 'true') ? mockSongs : [] +const demoPodcasts: Podcast[] = (import.meta.env.DEV || import.meta.env.VITE_DEMO_CONTENT === 'true') ? mockPodcasts : [] import { fetchRssFromUrls } from '@/composables/useRssFetch' import { extractAllFilms, extractAllSongs, extractAllPodcasts, extractAllBooks, @@ -592,7 +598,7 @@ export function useContentPanel() { } function showAllFilms() { - panelFilms.value = [...mockFilms] + panelFilms.value = [...demoFilms] panelSongs.value = [] panelPodcasts.value = [] panelTitle.value = 'Your Film Library' @@ -603,7 +609,7 @@ export function useContentPanel() { function showAllSongs() { panelFilms.value = [] - panelSongs.value = [...mockSongs] + panelSongs.value = [...demoSongs] panelPodcasts.value = [] panelTitle.value = 'Your Song Library' contentType.value = 'song' @@ -614,7 +620,7 @@ export function useContentPanel() { function showAllPodcasts() { panelFilms.value = [] panelSongs.value = [] - panelPodcasts.value = [...mockPodcasts] + panelPodcasts.value = [...demoPodcasts] panelTitle.value = 'Your Podcast Library' contentType.value = 'podcast' panelOpen.value = true diff --git a/aiui/packages/app/src/composables/useFederatedSearch.ts b/aiui/packages/app/src/composables/useFederatedSearch.ts index 7e41742e..d9eb6eb9 100644 --- a/aiui/packages/app/src/composables/useFederatedSearch.ts +++ b/aiui/packages/app/src/composables/useFederatedSearch.ts @@ -2,6 +2,12 @@ import { ref, watch } from 'vue' import { mockFilms } from '@/mocks/films' import { mockSongs } from '@/mocks/songs' import { mockPodcasts } from '@/mocks/podcasts' +import type { Film, Song, Podcast } from '@aiui/core/types/content' + +// Demo-site content pack (operator decision 2026-08-07): demo/dev only. +const demoFilms: Film[] = (import.meta.env.DEV || import.meta.env.VITE_DEMO_CONTENT === 'true') ? mockFilms : [] +const demoSongs: Song[] = (import.meta.env.DEV || import.meta.env.VITE_DEMO_CONTENT === 'true') ? mockSongs : [] +const demoPodcasts: Podcast[] = (import.meta.env.DEV || import.meta.env.VITE_DEMO_CONTENT === 'true') ? mockPodcasts : [] export interface SearchResult { type: 'film' | 'song' | 'podcast' @@ -21,7 +27,7 @@ function searchLibrary(q: string): SearchResult[] { const lower = q.toLowerCase() const matched: SearchResult[] = [] - for (const film of mockFilms) { + for (const film of demoFilms) { if ( film.title.toLowerCase().includes(lower) || film.director.toLowerCase().includes(lower) || @@ -37,7 +43,7 @@ function searchLibrary(q: string): SearchResult[] { } } - for (const song of mockSongs) { + for (const song of demoSongs) { if ( song.title.toLowerCase().includes(lower) || song.artist.toLowerCase().includes(lower) || @@ -53,7 +59,7 @@ function searchLibrary(q: string): SearchResult[] { } } - for (const podcast of mockPodcasts) { + for (const podcast of demoPodcasts) { if ( podcast.title.toLowerCase().includes(lower) || (podcast.host ?? '').toLowerCase().includes(lower) diff --git a/aiui/packages/app/src/plugins/mcp-server.ts b/aiui/packages/app/src/plugins/mcp-server.ts index 6b58f791..f44db6a6 100644 --- a/aiui/packages/app/src/plugins/mcp-server.ts +++ b/aiui/packages/app/src/plugins/mcp-server.ts @@ -6,6 +6,11 @@ import { mockFilms } from '@/mocks/films' import { mockSongs } from '@/mocks/songs' import { mockPodcasts } from '@/mocks/podcasts' +// Demo-site content pack (operator decision 2026-08-07): demo/dev only. +const demoFilms = (import.meta.env.DEV || import.meta.env.VITE_DEMO_CONTENT === 'true') ? mockFilms : [] +const demoSongs = (import.meta.env.DEV || import.meta.env.VITE_DEMO_CONTENT === 'true') ? mockSongs : [] +const demoPodcasts = (import.meta.env.DEV || import.meta.env.VITE_DEMO_CONTENT === 'true') ? mockPodcasts : [] + export interface MCPTool { name: string description: string @@ -97,7 +102,7 @@ type ToolHandler = (input: Record) => unknown const toolHandlers: Record = { search_films(input) { const query = (input.query as string ?? '').toLowerCase() - const results = mockFilms.filter(f => + const results = demoFilms.filter(f => f.title.toLowerCase().includes(query) || f.director.toLowerCase().includes(query) || f.genres.some(g => g.toLowerCase().includes(query)), @@ -117,7 +122,7 @@ const toolHandlers: Record = { search_songs(input) { const query = (input.query as string ?? '').toLowerCase() - const results = mockSongs.filter(s => + const results = demoSongs.filter(s => s.title.toLowerCase().includes(query) || s.artist.toLowerCase().includes(query) || (s.genres ?? []).some(g => g.toLowerCase().includes(query)), @@ -137,7 +142,7 @@ const toolHandlers: Record = { search_podcasts(input) { const query = (input.query as string ?? '').toLowerCase() - const results = mockPodcasts.filter(p => + const results = demoPodcasts.filter(p => p.title.toLowerCase().includes(query) || (p.host ?? '').toLowerCase().includes(query) || (p.genres ?? []).some(g => g.toLowerCase().includes(query)), @@ -156,13 +161,13 @@ const toolHandlers: Record = { get_library_stats() { return { - films: mockFilms.length, - songs: mockSongs.length, - podcasts: mockPodcasts.length, + films: demoFilms.length, + songs: demoSongs.length, + podcasts: demoPodcasts.length, genres: { - film: [...new Set(mockFilms.flatMap(f => f.genres))].sort(), - song: [...new Set(mockSongs.flatMap(s => s.genres ?? []))].sort(), - podcast: [...new Set(mockPodcasts.flatMap(p => p.genres ?? []))].sort(), + film: [...new Set(demoFilms.flatMap(f => f.genres))].sort(), + song: [...new Set(demoSongs.flatMap(s => s.genres ?? []))].sort(), + podcast: [...new Set(demoPodcasts.flatMap(p => p.genres ?? []))].sort(), }, } }, diff --git a/aiui/packages/app/vite.config.ts b/aiui/packages/app/vite.config.ts index 5653b264..75481697 100644 --- a/aiui/packages/app/vite.config.ts +++ b/aiui/packages/app/vite.config.ts @@ -2,7 +2,7 @@ import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' import tailwindcss from '@tailwindcss/vite' import { VitePWA } from 'vite-plugin-pwa' -import { resolve } from 'path' +import { resolve, sep } from 'path' import { tmdbPlugin } from './vite-tmdb' import { devChatsPlugin } from './vite-dev-chats' import { musicSearchPlugin } from './vite-music-search' @@ -12,6 +12,14 @@ import { fsPlugin } from './vite-fs' export default defineConfig({ base: process.env.VITE_BASE_PATH || '/', + // Always define VITE_DEMO_CONTENT as a string literal so the + // DEMO_CONTENT_ENABLED gate constant-folds at build time (an UNDEFINED + // env var leaves `undefined === 'true'` in the bundle, which rollup's + // treeshaker refuses to evaluate — that is how mock hosts survived in + // the 2026-08-07 RC1 prod bundle despite the gate). + define: { + 'import.meta.env.VITE_DEMO_CONTENT': JSON.stringify(process.env.VITE_DEMO_CONTENT ?? 'false'), + }, plugins: [ vue(), tailwindcss(), @@ -129,6 +137,19 @@ export default defineConfig({ '@aiui/core': resolve(__dirname, '../core/src'), }, }, + build: { + rollupOptions: { + treeshake: { + // Mock modules are pure data by design — but a module-level + // `allGenres = [...new Set(mockFilms.flatMap(...))]` export made + // films.ts look side-effectful, so the treeshaker kept the whole + // module (and every mock host string) in the prod bundle even with + // zero live references. Declaring the mocks directory + // side-effect-free lets the demo-content gate actually drop them. + moduleSideEffects: (id) => !id.includes(`${sep}src${sep}mocks${sep}`), + }, + }, + }, server: { port: 5173, host: process.env.VITE_HOST === 'false' ? 'localhost' : true,