diff --git a/src/assets/images/link.png b/src/assets/images/link.png new file mode 100644 index 0000000..2e14709 Binary files /dev/null and b/src/assets/images/link.png differ diff --git a/src/assets/images/search.png b/src/assets/images/search.png new file mode 100644 index 0000000..b0b84e6 Binary files /dev/null and b/src/assets/images/search.png differ diff --git a/src/assets/images/web-card.png b/src/assets/images/web-card.png new file mode 100644 index 0000000..47351d5 Binary files /dev/null and b/src/assets/images/web-card.png differ diff --git a/src/utils/useWebTitle.ts b/src/utils/useWebTitle.ts new file mode 100644 index 0000000..5bf7367 --- /dev/null +++ b/src/utils/useWebTitle.ts @@ -0,0 +1,92 @@ +import { ref } from 'vue' + +export const useWebsiteTitle = () => { + const titles = ref>(new Map()) + const loading = ref(new Set()) + const errors = ref(new Map()) + + // 新增:重试配置 + const MAX_RETRY = 3 + const BASE_DELAY = 800 // 毫秒 + + const getCache = (url: string) => { + const cached = sessionStorage.getItem(`title_cache_${url}`) + if (!cached) return null + const { title, expire } = JSON.parse(cached) + return Date.now() < expire ? title : null + } + + const setCache = (url: string, title: string) => { + const data = { title, expire: Date.now() + 7 * 24 * 60 * 60 * 1000 } + sessionStorage.setItem(`title_cache_${url}`, JSON.stringify(data)) + } + + const fetchWithRetry = async (url: string, retryCount = 0): Promise => { + const proxyUrl = `https://api.allorigins.win/raw?url=${encodeURIComponent(url)}` + + try { + const res = await fetch(proxyUrl) + + if (!res.ok) { + throw new Error(`HTTP ${res.status}`) + } + + const html = await res.text() + const parser = new DOMParser() + const doc = parser.parseFromString(html, 'text/html') + const title = + doc.querySelector('title')?.textContent?.trim() || new URL(url).hostname || '无标题' + + return title + } catch (err) { + if (retryCount >= MAX_RETRY) { + throw err // 达到最大重试次数,抛出错误 + } + + // 第1次等800ms,第2次等1600ms,第3次等3200ms + const delay = BASE_DELAY * Math.pow(2, retryCount) + console.warn( + `获取标题失败,${retryCount + 1}/${MAX_RETRY} 次重试,等待 ${delay}ms`, + url + ) + + await new Promise((resolve) => setTimeout(resolve, delay)) + return fetchWithRetry(url, retryCount + 1) + } + } + + const fetchTitle = async (url: string): Promise => { + if (titles.value.has(url)) return titles.value.get(url)! + + const cached = getCache(url) + if (cached) { + titles.value.set(url, cached) + return cached + } + + if (loading.value.has(url)) return '' + + loading.value.add(url) + errors.value.delete(url) + + try { + const title = await fetchWithRetry(url) + titles.value.set(url, title) + setCache(url, title) + return title + } catch (err) { + const msg = `获取标题失败(已重试 ${MAX_RETRY} 次)` + errors.value.set(url, msg) + console.error(err) + return msg + } finally { + loading.value.delete(url) + } + } + + const fetchAll = async (urls: string[]) => { + await Promise.allSettled(urls.map(fetchTitle)) + } + + return { titles, loading, errors, fetchTitle, fetchAll } +} diff --git a/src/views/home/agent/components/Agent.vue b/src/views/home/agent/components/Agent.vue index f654e30..989ce44 100644 --- a/src/views/home/agent/components/Agent.vue +++ b/src/views/home/agent/components/Agent.vue @@ -210,7 +210,6 @@ // 流式响应处理 let contentBody = '' let buffer = '' - const webAddressList = [] const reader = response.body?.getReader() if (!reader) throw new Error('无法获取流读取器') @@ -248,10 +247,8 @@ ?.trim() || '' if (!hasReportStarted && eventName === 'report') { - console.log('开始生成报告--------') - isGeneratingReport.value = true - contentBody += `123` + contentBody += `123` hasReportStarted = true } @@ -298,9 +295,9 @@ params.versionID = dataLines[0] projectStore.setProject({ nodeId: dataLines[0] }) } - if (eventName === 'webAddress') { - console.log('webAddress-----', eventName, dataLines) - } + // if (eventName === 'webAddress') { + // console.log('webAddress111111111111111', eventName, dataLines) + // } if (eventName === 'tool') { MyEvent.emit('loading-sketch') @@ -313,7 +310,7 @@ const jsonData = JSON.parse(jsonText) // console.log('jsonData', jsonData) if (jsonData.webAddress) { - console.log('webAddress-----', jsonData) + aiMessage.webAddress = JSON.parse(jsonData.webAddress) } if (jsonData.title) { emits('setTitle', jsonData.title) @@ -439,7 +436,10 @@ while (i < dialogue.length) { const item = dialogue[i] - + if (item.webAddress?.length > 0) { + console.log('item.webAddress-----', item.webAddress) + debugger + } if (item.role === 'user') { // user 角色直接添加 result.push({ @@ -496,7 +496,7 @@ for (let i = 0; i < session.dialogue.length; i++) { if (session.dialogue[i].report) { session.dialogue[i].content = - `123` + + `123` + (session.dialogue[i].content || '') break } @@ -516,7 +516,7 @@ // 3. 收集 sketchIDAndUrl 到 imgList if (session.sketchIDAndUrl) { - imgList.push(...session.sketchIDAndUrl) + imgList.push(session.sketchIDAndUrl) } // 4. 处理 dialogue diff --git a/src/views/home/agent/components/Item.vue b/src/views/home/agent/components/Item.vue index fa08495..8ab6681 100644 --- a/src/views/home/agent/components/Item.vue +++ b/src/views/home/agent/components/Item.vue @@ -40,10 +40,21 @@ + + +
+ + {{ content.webAddress?.length }} web pages have been retrieved. +
@@ -96,13 +107,13 @@ isLast: Boolean }>() - watch( - () => props.content, - (newVal) => { - console.log('newVal-----', newVal) - }, - { immediate: true } - ) + // watch( + // () => props.content, + // (newVal) => { + // console.log('newVal-----', newVal) + // }, + // { immediate: true } + // ) const emit = defineEmits(['regenerate']) @@ -215,6 +226,7 @@ // 点击显示报告 } const handleClickUrls = (data) => { + MyEvent.emit('openUrls', props.content.webAddress) // 点击显示来源 } @@ -260,6 +272,24 @@ width: fit-content; max-width: 82%; } + .web-address { + width: fit-content; + min-width: 22.5rem; + line-height: 2.6rem; + padding: 0 1rem; + border-radius: 1.5rem; + color: #000000a6; + border: 0.1rem solid #0000001a; + font-family: 'Regular'; + font-weight: 400; + font-size: 1.2rem; + margin-top: 1rem; + column-gap: 0.8rem; + .search-icon { + width: 1.4rem; + height: 1.4rem; + } + } } .operate { margin-top: 1.3rem; diff --git a/src/views/home/agent/components/Preview.vue b/src/views/home/agent/components/Preview.vue index b510bf2..45805ac 100644 --- a/src/views/home/agent/components/Preview.vue +++ b/src/views/home/agent/components/Preview.vue @@ -52,27 +52,34 @@
-
+ + +
@@ -92,6 +99,9 @@ import { VueMarkdown } from '@crazydos/vue-markdown' import type { CustomAttrs } from '@crazydos/vue-markdown' import rehypeRaw from 'rehype-raw' + import { useWebsiteTitle } from '@/utils/useWebTitle' + + const { titles, loading, fetchAll } = useWebsiteTitle() const { t } = useI18n() const emits = defineEmits(['deleteSketch']) @@ -125,17 +135,29 @@ const sessionId = ref('') const markdownContent = ref('') -const setSessionId = (id: string) => { - + const urlList = ref([]) + const reportType = ref<'report' | 'urls'>('report') + const setSessionId = (id: string) => { + console.log('setSessionId-----', id) + reportType.value = 'report' sessionId.value = id } + const setUrls = (list: string[]) => { + console.log('setUrls-----', list) + reportType.value = 'urls' + urlList.value = [ + 'https://furnitureindustrynews.substack.com/p/what-2026-really-looks-like-for-furniture', + 'https://furnitureindustrynews.substack.com/p/what-2026-really-looks-like-for-furniture', + 'https://furnitureindustrynews.substack.com/p/what-2026-really-looks-like-for-furniture' + ] + fetchAll(list) + } watch( () => sessionId.value, (newVal) => { if (newVal) { markdownContent.value = localStorage.getItem(`reportsContent_${newVal}`) - console.log('markdownContent-----', markdownContent.value); - + console.log(`报告key值:reportsContent_${newVal}`, markdownContent.value) } } ) @@ -143,9 +165,7 @@ const setSessionId = (id: string) => { // 图片加载完成时触发 const handleImageLoad = (index: number) => { loadedStatus[index] = true - if (index === props.sketchList.length - 1) { - showLoading.value = false - } + showLoading.value = false } // 获取当前显示的图片源 @@ -209,6 +229,8 @@ const setSessionId = (id: string) => { watch( () => props.sketchList, (val) => { + console.log('sketchList-----', val); + if (val.length > 0) { showLoading.value = false } @@ -223,7 +245,8 @@ const setSessionId = (id: string) => { }) defineExpose({ - setSessionId + setSessionId, + setUrls }) @@ -289,6 +312,7 @@ const setSessionId = (id: string) => { flex: 1; overflow: hidden; height: 100%; + &::before { content: ''; position: absolute; @@ -348,6 +372,7 @@ const setSessionId = (id: string) => { white-space: pre-wrap; overflow-y: auto; margin: 2rem; + padding: 0 8.8rem 8.8rem; } } } diff --git a/src/views/home/agent/components/ReportCard.vue b/src/views/home/agent/components/ReportCard.vue index 6e0b133..e13210d 100644 --- a/src/views/home/agent/components/ReportCard.vue +++ b/src/views/home/agent/components/ReportCard.vue @@ -1,49 +1,79 @@ diff --git a/src/views/home/agent/components/UrlCard.vue b/src/views/home/agent/components/UrlCard.vue index af3e6fd..ccc3702 100644 --- a/src/views/home/agent/components/UrlCard.vue +++ b/src/views/home/agent/components/UrlCard.vue @@ -1,5 +1,5 @@