feat(chat): enhance chat functionality with web search and article integration
- Updated ChatMessage and ChatWindow components to support inline web search results and articles. - Integrated new web search and RSS plugins into the chat system for real-time information retrieval. - Enhanced useContentPanel to manage web search results alongside existing media types. - Added ArticleOverlay component for displaying selected articles from search results. - Improved UI elements and styles for better user interaction with web search features. Made-with: Cursor
This commit is contained in:
@@ -39,6 +39,24 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="inlineNewsLinks.length > 0" class="mt-3 space-y-1" @click.stop>
|
||||
<NewsCard
|
||||
v-for="(link, i) in inlineNewsLinks"
|
||||
:key="i"
|
||||
:article="link"
|
||||
@select-article="handleArticleSelect"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="inlineWebsitesLinks.length > 0" class="mt-3 space-y-1" @click.stop>
|
||||
<NewsCard
|
||||
v-for="(link, i) in inlineWebsitesLinks"
|
||||
:key="`web-${i}`"
|
||||
:article="link"
|
||||
@select-article="handleWebsiteSelect"
|
||||
/>
|
||||
</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>
|
||||
@@ -63,6 +81,20 @@
|
||||
>
|
||||
View all {{ inlinePodcasts.length }} podcasts →
|
||||
</button>
|
||||
<button
|
||||
v-else-if="inlineNewsLinks.length > 1"
|
||||
class="text-[10px] text-accent/70 hover:text-accent transition-colors"
|
||||
@click.stop="openPanel"
|
||||
>
|
||||
View all {{ inlineNewsLinks.length }} articles →
|
||||
</button>
|
||||
<button
|
||||
v-else-if="inlineWebsitesLinks.length > 1"
|
||||
class="text-[10px] text-accent/70 hover:text-accent transition-colors"
|
||||
@click.stop="openPanel"
|
||||
>
|
||||
View all {{ inlineWebsitesLinks.length }} websites →
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -70,50 +102,61 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import type { Message } from '@aiui/core/types/message'
|
||||
import type { Message, WebSearchResult } from '@aiui/core/types/message'
|
||||
import type { Film, Song, Podcast } from '@aiui/core/types/content'
|
||||
import { useTheme } from '@/composables/useTheme'
|
||||
import { useContentPanel } from '@/composables/useContentPanel'
|
||||
import { useArticleOverlayStore } from '@/stores/articleOverlay'
|
||||
import FilmCard from '@/components/content/FilmCard.vue'
|
||||
import SongCard from '@/components/content/SongCard.vue'
|
||||
import PodcastCard from '@/components/content/PodcastCard.vue'
|
||||
import NewsCard from '@/components/content/NewsCard.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
message: Message
|
||||
index: number
|
||||
}>()
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
message: Message
|
||||
index: number
|
||||
triggeringQuery?: string
|
||||
}>(),
|
||||
{ triggeringQuery: '' }
|
||||
)
|
||||
|
||||
const { isDark } = useTheme()
|
||||
const { extractAllFilms, extractAllSongs, extractAllPodcasts, stripContentTags, updatePanelFromText, openFilmDetail, openSongDetail, openPodcastDetail, closeFilmDetail, closeSongDetail, closePodcastDetail } = useContentPanel()
|
||||
const { getContextualInlineContent, stripContentTags, stripMarkdownLinks, updatePanelFromText, openFilmDetail, openSongDetail, openPodcastDetail, openArticleDetail, closeFilmDetail, closeSongDetail, closePodcastDetail } = useContentPanel()
|
||||
const overlayStore = useArticleOverlayStore()
|
||||
|
||||
const isUser = computed(() => props.message.role === 'user')
|
||||
|
||||
const inlineContent = computed(() => {
|
||||
if (isUser.value) return { films: [] as Film[], songs: [] as Song[], podcasts: [] as Podcast[], newsLinks: [], websitesLinks: [] }
|
||||
return getContextualInlineContent(props.message.content, props.triggeringQuery, props.message.webResults ?? [])
|
||||
})
|
||||
|
||||
const bubbleClasses = computed(() =>
|
||||
isUser.value
|
||||
? 'path-glass-bubble-user rounded-br-md rounded-2xl'
|
||||
: 'path-glass-bubble rounded-bl-md rounded-2xl'
|
||||
)
|
||||
|
||||
const inlineFilms = computed(() => {
|
||||
if (isUser.value) return []
|
||||
return extractAllFilms(props.message.content)
|
||||
})
|
||||
const inlineFilms = computed(() => inlineContent.value.films)
|
||||
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 inlineSongs = computed(() => {
|
||||
if (isUser.value) return []
|
||||
return extractAllSongs(props.message.content)
|
||||
})
|
||||
|
||||
const inlinePodcasts = computed(() => {
|
||||
if (isUser.value) return []
|
||||
return extractAllPodcasts(props.message.content)
|
||||
})
|
||||
|
||||
const hasContext = computed(() => !isUser.value && (inlineFilms.value.length > 0 || inlineSongs.value.length > 0 || inlinePodcasts.value.length > 0))
|
||||
const hasContext = computed(() => !isUser.value && (
|
||||
inlineFilms.value.length > 0 ||
|
||||
inlineSongs.value.length > 0 ||
|
||||
inlinePodcasts.value.length > 0 ||
|
||||
inlineNewsLinks.value.length > 0 ||
|
||||
inlineWebsitesLinks.value.length > 0
|
||||
))
|
||||
|
||||
const displayText = computed(() => {
|
||||
if (isUser.value) return props.message.content
|
||||
return stripContentTags(props.message.content)
|
||||
let text = stripContentTags(props.message.content)
|
||||
if (inlineNewsLinks.value.length > 0 || inlineWebsitesLinks.value.length > 0) text = stripMarkdownLinks(text)
|
||||
return text
|
||||
})
|
||||
|
||||
const formattedTime = computed(() => {
|
||||
@@ -121,22 +164,26 @@ const formattedTime = computed(() => {
|
||||
return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
|
||||
})
|
||||
|
||||
function openPanelWithContext() {
|
||||
updatePanelFromText(props.message.content, props.triggeringQuery, props.message.webResults ?? [])
|
||||
}
|
||||
|
||||
function handleFilmSelect(film: Film) {
|
||||
updatePanelFromText(props.message.content)
|
||||
openPanelWithContext()
|
||||
closeSongDetail()
|
||||
closePodcastDetail()
|
||||
openFilmDetail(film)
|
||||
}
|
||||
|
||||
function handleSongSelect(song: Song) {
|
||||
updatePanelFromText(props.message.content)
|
||||
openPanelWithContext()
|
||||
closeFilmDetail()
|
||||
closePodcastDetail()
|
||||
openSongDetail(song)
|
||||
}
|
||||
|
||||
function handlePodcastSelect(podcast: Podcast) {
|
||||
updatePanelFromText(props.message.content)
|
||||
openPanelWithContext()
|
||||
closeFilmDetail()
|
||||
closeSongDetail()
|
||||
openPodcastDetail(podcast)
|
||||
@@ -146,7 +193,16 @@ function openPanel() {
|
||||
closeFilmDetail()
|
||||
closeSongDetail()
|
||||
closePodcastDetail()
|
||||
updatePanelFromText(props.message.content)
|
||||
openPanelWithContext()
|
||||
}
|
||||
|
||||
function handleArticleSelect(article: WebSearchResult) {
|
||||
openPanelWithContext()
|
||||
openArticleDetail(article)
|
||||
}
|
||||
|
||||
function handleWebsiteSelect(article: WebSearchResult) {
|
||||
overlayStore.open(article.url, article.title, undefined, article.imgSrc)
|
||||
}
|
||||
|
||||
function handleBubbleClick() {
|
||||
|
||||
Reference in New Issue
Block a user