feat(chat): enhance film integration and UI improvements

- Updated ChatMessage component to display inline film cards and added functionality for selecting films.
- Improved ChatWindow to handle film-related content and update the panel with selected films.
- Refactored useAI to streamline film recommendation prompts and context.
- Enhanced ChatPage layout for better film library access and user experience.
- Updated service worker revision for PWA improvements.

Made-with: Cursor
This commit is contained in:
Dorian
2026-03-02 16:48:17 +00:00
parent ece4b8256f
commit 464069b2f2
13 changed files with 658 additions and 112 deletions
@@ -2,6 +2,11 @@
<div class="p-3 md:p-4">
<div
class="glass rounded-2xl px-4 py-3 flex items-end gap-3 transition-all duration-300"
:class="focused
? isDark
? 'border-white/30'
: 'border-black/20'
: ''"
>
<textarea
ref="textareaRef"
@@ -14,6 +19,8 @@
: 'text-gray-800 placeholder:text-gray-400'"
@keydown.enter.exact.prevent="send"
@input="autoResize"
@focus="focused = true"
@blur="focused = false"
/>
<button
:disabled="!canSend"
@@ -53,6 +60,7 @@ const emit = defineEmits<{
const { isDark } = useTheme()
const text = ref('')
const focused = ref(false)
const textareaRef = ref<HTMLTextAreaElement | null>(null)
const canSend = computed(() => text.value.trim().length > 0 && !props.disabled)
@@ -5,14 +5,31 @@
:style="{ animationDelay: `${index * 30}ms` }"
>
<div
class="max-w-[85%] md:max-w-[70%] rounded-2xl px-4 py-3 transition-all duration-300"
class="max-w-[85%] md:max-w-[75%] rounded-2xl px-4 py-3 transition-all duration-300"
:class="bubbleClasses"
>
<p class="text-sm leading-relaxed whitespace-pre-wrap break-words"
:class="isDark ? 'text-white/90' : 'text-gray-800'">{{ message.content }}</p>
: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>
@@ -21,7 +38,10 @@
<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
@@ -29,6 +49,8 @@ const props = defineProps<{
}>()
const { isDark } = useTheme()
const { extractFilmIds, resolveFilms, stripFilmTags, updatePanelFromText, openFilmDetail } = useContentPanel()
const isUser = computed(() => props.message.role === 'user')
const bubbleClasses = computed(() =>
@@ -37,8 +59,28 @@ const bubbleClasses = computed(() =>
: 'glass-card rounded-bl-md'
)
const inlineFilms = computed(() => {
if (isUser.value) return []
const ids = extractFilmIds(props.message.content)
return resolveFilms(ids)
})
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)
}
</script>
@@ -48,6 +48,7 @@ import { computed, ref, watch, nextTick } from 'vue'
import { useChatStore } from '@/stores/chat'
import { useAI } from '@/composables/useAI'
import { useTheme } from '@/composables/useTheme'
import { useContentPanel } from '@/composables/useContentPanel'
import ChatHeader from './ChatHeader.vue'
import ChatMessage from './ChatMessage.vue'
import ChatInput from './ChatInput.vue'
@@ -74,6 +75,7 @@ defineEmits<{
const chatStore = useChatStore()
const { sendMessage } = useAI()
const { isDark } = useTheme()
const { updatePanelFromText } = useContentPanel()
const messageListRef = ref<HTMLElement | null>(null)
const messages = computed(() => chatStore.messages)
@@ -113,11 +115,14 @@ watch(
watch(
() => messages.value[messages.value.length - 1]?.content,
() => {
(content) => {
nextTick(() => {
const el = messageListRef.value
if (el) el.scrollTop = el.scrollHeight
})
if (content && !isStreaming.value) {
updatePanelFromText(content)
}
}
)
</script>