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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user