feat(content): add places, code mode, mobile context tab, and detail views

- Add Places/Restaurants content type with PlaceCard, PlaceDetail, PlaceGrid
- Add WebsiteDetail and MagazineSectionDetail views for Context panel
- Enhance MagazineGrid hero with background image and 3x taller header
- Add mobile 3-tab layout (Chat, Content, Context) with detail navigation
- Add /code command system: useCodeContext composable, ProjectGrid, FileTreeNode,
  CodeDetail for IDE-style code viewing across all three panels
- Fix /code bubble and prompt index clicks to re-activate code mode
- Fix updatePanelFromText overwriting code tab by skipping command messages

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-03 09:31:18 +00:00
co-authored by Claude Opus 4.6
parent 48dd7a9c68
commit e8fc54cade
22 changed files with 1901 additions and 107 deletions
@@ -57,6 +57,15 @@
/>
</div>
<div v-if="inlinePlaces.length > 0" class="mt-3 space-y-1" @click.stop>
<PlaceCard
v-for="place in inlinePlaces"
:key="place.id"
:place="place"
@select="handlePlaceSelect"
/>
</div>
<div v-if="inlineNewsLinks.length > 0" class="mt-3 space-y-1" @click.stop>
<NewsCard
v-for="(link, i) in inlineNewsLinks"
@@ -106,6 +115,13 @@
>
View all {{ inlineImages.length }} images
</button>
<button
v-else-if="inlinePlaces.length > 1"
class="text-[10px] text-accent/70 hover:text-accent transition-colors"
@click.stop="openPanel"
>
View all {{ inlinePlaces.length }} places
</button>
<button
v-else-if="inlineSongs.length > 1"
class="text-[10px] text-accent/70 hover:text-accent transition-colors"
@@ -149,15 +165,16 @@
<script setup lang="ts">
import { computed } from 'vue'
import type { Message, WebSearchResult } from '@aiui/core/types/message'
import type { Film, Song, Podcast, Book, TVSeries, ImageItem } from '@aiui/core/types/content'
import type { Film, Song, Podcast, Book, TVSeries, ImageItem, Place } from '@aiui/core/types/content'
import { useTheme } from '@/composables/useTheme'
import { useContentPanel, type MagazineSection } from '@/composables/useContentPanel'
import { useArticleOverlayStore } from '@/stores/articleOverlay'
import { useCodeContext } from '@/composables/useCodeContext'
import FilmCard from '@/components/content/FilmCard.vue'
import BookCard from '@/components/content/BookCard.vue'
import TVSeriesCard from '@/components/content/TVSeriesCard.vue'
import SongCard from '@/components/content/SongCard.vue'
import PodcastCard from '@/components/content/PodcastCard.vue'
import PlaceCard from '@/components/content/PlaceCard.vue'
import NewsCard from '@/components/content/NewsCard.vue'
const props = withDefaults(
@@ -170,13 +187,13 @@ const props = withDefaults(
)
const { isDark } = useTheme()
const { getContextualInlineContent, stripContentTags, stripMarkdownLinks, updatePanelFromText, openFilmDetail, openBookDetail, openTVSeriesDetail, openImageDetail, openSongDetail, openPodcastDetail, openArticleDetail, closeFilmDetail, closeBookDetail, closeTVSeriesDetail, closeImageDetail, closeSongDetail, closePodcastDetail } = useContentPanel()
const overlayStore = useArticleOverlayStore()
const { getContextualInlineContent, stripContentTags, stripMarkdownLinks, updatePanelFromText, panelOpen, availableTabs, setActiveTab, openFilmDetail, openBookDetail, openTVSeriesDetail, openImageDetail, openPlaceDetail, openSongDetail, openPodcastDetail, openArticleDetail, openWebsiteDetail, closeFilmDetail, closeBookDetail, closeTVSeriesDetail, closeImageDetail, closePlaceDetail, closeSongDetail, closePodcastDetail } = useContentPanel()
const codeContext = useCodeContext()
const isUser = computed(() => props.message.role === 'user')
const inlineContent = computed(() => {
if (isUser.value) return { films: [] as Film[], books: [] as Book[], tvSeries: [] as TVSeries[], images: [] as ImageItem[], songs: [] as Song[], podcasts: [] as Podcast[], newsLinks: [] as WebSearchResult[], websitesLinks: [] as WebSearchResult[], magazineSections: [] as MagazineSection[] }
if (isUser.value) return { films: [] as Film[], books: [] as Book[], tvSeries: [] as TVSeries[], images: [] as ImageItem[], places: [] as Place[], songs: [] as Song[], podcasts: [] as Podcast[], newsLinks: [] as WebSearchResult[], websitesLinks: [] as WebSearchResult[], magazineSections: [] as MagazineSection[] }
return getContextualInlineContent(props.message.content, props.triggeringQuery, props.message.webResults ?? [])
})
@@ -190,13 +207,19 @@ const inlineFilms = computed(() => inlineContent.value.films)
const inlineBooks = computed(() => inlineContent.value.books ?? [])
const inlineTVSeries = computed(() => inlineContent.value.tvSeries ?? [])
const inlineImages = computed(() => inlineContent.value.images ?? [])
const inlinePlaces = computed(() => inlineContent.value.places ?? [])
const inlineSongs = computed(() => inlineContent.value.songs)
const inlinePodcasts = computed(() => inlineContent.value.podcasts)
const inlineNewsLinks = computed(() => inlineContent.value.newsLinks ?? [])
const inlineWebsitesLinks = computed(() => inlineContent.value.websitesLinks ?? [])
const inlineMagazineSections = computed(() => inlineContent.value.magazineSections ?? [])
const isCodeResponse = computed(() =>
!isUser.value && props.triggeringQuery.trim().toLowerCase() === '/code'
)
const hasContext = computed(() => !isUser.value && (
isCodeResponse.value ||
inlineFilms.value.length > 0 ||
inlineBooks.value.length > 0 ||
inlineTVSeries.value.length > 0 ||
@@ -257,6 +280,14 @@ function handleSongSelect(song: Song) {
openSongDetail(song)
}
function handlePlaceSelect(place: Place) {
openPanelWithContext()
closeFilmDetail()
closeBookDetail()
closePlaceDetail()
openPlaceDetail(place)
}
function handlePodcastSelect(podcast: Podcast) {
openPanelWithContext()
closeFilmDetail()
@@ -279,10 +310,24 @@ function handleArticleSelect(article: WebSearchResult) {
}
function handleWebsiteSelect(article: WebSearchResult) {
overlayStore.open(article.url, article.title, undefined, article.imgSrc)
openPanelWithContext()
openWebsiteDetail(article)
}
function activateCodeMode() {
codeContext.enterCodeMode()
panelOpen.value = true
if (!availableTabs.value.includes('code')) {
availableTabs.value = [...availableTabs.value, 'code']
}
setActiveTab('code')
}
function handleBubbleClick() {
if (isCodeResponse.value) {
activateCodeMode()
return
}
if (hasContext.value) openPanel()
}
</script>