- Updated ChatMessage component to support click interactions for film context. - Added computed property to determine if a message has associated films. - Refactored film ID extraction to normalize film IDs for consistency. Made-with: Cursor
94 lines
2.7 KiB
Vue
94 lines
2.7 KiB
Vue
<template>
|
|
<div
|
|
class="flex animate-fade-up-fast"
|
|
:class="isUser ? 'justify-end' : 'justify-start'"
|
|
:style="{ animationDelay: `${index * 30}ms` }"
|
|
>
|
|
<div
|
|
class="max-w-[85%] md:max-w-[75%] rounded-2xl px-4 py-3 transition-all duration-300"
|
|
:class="[bubbleClasses, { 'cursor-pointer': hasContext }]"
|
|
@click="handleBubbleClick"
|
|
>
|
|
<p class="text-sm leading-relaxed whitespace-pre-wrap break-words"
|
|
:class="isDark ? 'text-white/90' : 'text-gray-800'">{{ displayText }}</p>
|
|
|
|
<div v-if="inlineFilms.length > 0" class="mt-3 space-y-1">
|
|
<FilmCard
|
|
v-for="film in inlineFilms"
|
|
:key="film.id"
|
|
:film="film"
|
|
@select="handleFilmSelect"
|
|
/>
|
|
</div>
|
|
|
|
<div class="flex items-center gap-2 mt-1.5">
|
|
<span class="text-[10px] select-none"
|
|
:class="isDark ? 'text-white/30' : 'text-gray-400'">{{ formattedTime }}</span>
|
|
<button
|
|
v-if="inlineFilms.length > 1"
|
|
class="text-[10px] text-accent/70 hover:text-accent transition-colors"
|
|
@click="openPanel"
|
|
>
|
|
View all {{ inlineFilms.length }} films →
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { computed } from 'vue'
|
|
import type { Message } from '@aiui/core/types/message'
|
|
import type { Film } from '@aiui/core/types/content'
|
|
import { useTheme } from '@/composables/useTheme'
|
|
import { useContentPanel } from '@/composables/useContentPanel'
|
|
import FilmCard from '@/components/content/FilmCard.vue'
|
|
|
|
const props = defineProps<{
|
|
message: Message
|
|
index: number
|
|
}>()
|
|
|
|
const { isDark } = useTheme()
|
|
const { extractFilmIds, resolveFilms, stripFilmTags, updatePanelFromText, openFilmDetail } = useContentPanel()
|
|
|
|
const isUser = computed(() => props.message.role === 'user')
|
|
|
|
const bubbleClasses = computed(() =>
|
|
isUser.value
|
|
? 'glass-strong rounded-br-md'
|
|
: 'glass-card rounded-bl-md'
|
|
)
|
|
|
|
const inlineFilms = computed(() => {
|
|
if (isUser.value) return []
|
|
const ids = extractFilmIds(props.message.content)
|
|
return resolveFilms(ids)
|
|
})
|
|
|
|
const hasContext = computed(() => !isUser.value && inlineFilms.value.length > 0)
|
|
|
|
const displayText = computed(() => {
|
|
if (isUser.value) return props.message.content
|
|
return stripFilmTags(props.message.content)
|
|
})
|
|
|
|
const formattedTime = computed(() => {
|
|
const d = new Date(props.message.timestamp)
|
|
return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
|
|
})
|
|
|
|
function handleFilmSelect(film: Film) {
|
|
updatePanelFromText(props.message.content)
|
|
openFilmDetail(film)
|
|
}
|
|
|
|
function openPanel() {
|
|
updatePanelFromText(props.message.content)
|
|
}
|
|
|
|
function handleBubbleClick() {
|
|
if (hasContext.value) openPanel()
|
|
}
|
|
</script>
|