fix(chat): polish references drawer layout and web reference titles
Improve reference list readability by resolving URL-like titles to domains, stripping markdown noise from snippets, and aligning suggestion chip hover styles across chat and embed surfaces.
This commit is contained in:
@@ -10,8 +10,9 @@
|
||||
>
|
||||
<header class="chat-references-panel__header">
|
||||
<div class="chat-references-panel__heading">
|
||||
<h3 class="chat-references-panel__title">{{ panelTitle }}</h3>
|
||||
<span v-if="totalCount" class="chat-references-panel__count">{{ totalCount }}</span>
|
||||
<h3 class="chat-references-panel__title">
|
||||
{{ panelTitle }}<span v-if="totalCount" class="chat-references-panel__count"> · {{ totalCount }}</span>
|
||||
</h3>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
@@ -19,7 +20,7 @@
|
||||
:aria-label="t('common.close')"
|
||||
@click="close"
|
||||
>
|
||||
<t-icon name="close" />
|
||||
<t-icon name="close" size="20px" />
|
||||
</button>
|
||||
</header>
|
||||
|
||||
@@ -41,17 +42,17 @@
|
||||
v-for="item in section.items"
|
||||
:key="item.key"
|
||||
:ref="(el) => setItemRef(item.key, el as HTMLElement | null)"
|
||||
class="reference-card"
|
||||
class="reference-item"
|
||||
:class="{
|
||||
'reference-card--web': item.kind === 'web',
|
||||
'reference-card--document': item.kind === 'document',
|
||||
'reference-card--tool': item.kind === 'tool',
|
||||
'reference-item--web': item.kind === 'web',
|
||||
'reference-item--document': item.kind === 'document',
|
||||
'reference-item--tool': item.kind === 'tool',
|
||||
'is-highlighted': item.key === activeHighlightKey,
|
||||
}"
|
||||
>
|
||||
<component
|
||||
:is="item.kind === 'web' ? 'a' : 'div'"
|
||||
class="reference-card__inner"
|
||||
class="reference-item__body"
|
||||
:class="{ 'is-expandable': item.kind === 'document' && hasMoreContent(item) }"
|
||||
:href="item.kind === 'web' ? item.url : undefined"
|
||||
:target="item.kind === 'web' ? '_blank' : undefined"
|
||||
@@ -63,60 +64,60 @@
|
||||
@keydown.enter="item.kind === 'document' && hasMoreContent(item) ? toggleDocumentSnippet(item) : undefined"
|
||||
@keydown.space.prevent="item.kind === 'document' && hasMoreContent(item) ? toggleDocumentSnippet(item) : undefined"
|
||||
>
|
||||
<div class="reference-card__meta">
|
||||
<span class="reference-card__index">{{ item.index }}</span>
|
||||
<img
|
||||
v-if="item.kind === 'web' && item.faviconUrl"
|
||||
class="reference-card__favicon"
|
||||
:src="item.faviconUrl"
|
||||
alt=""
|
||||
loading="lazy"
|
||||
@error="onFaviconError"
|
||||
/>
|
||||
<t-icon
|
||||
v-else-if="item.kind === 'document'"
|
||||
name="file"
|
||||
class="reference-card__doc-icon"
|
||||
/>
|
||||
<t-icon
|
||||
v-else-if="item.kind === 'tool'"
|
||||
name="tools"
|
||||
class="reference-card__doc-icon"
|
||||
/>
|
||||
<span v-if="item.domain" class="reference-card__domain">{{ item.domain }}</span>
|
||||
<span v-else-if="item.kind === 'document'" class="reference-card__domain">
|
||||
{{ t('chat.referencesDrawerDocument') }}
|
||||
</span>
|
||||
<span v-else-if="item.kind === 'tool'" class="reference-card__domain">
|
||||
{{ t('chat.referencesDrawerTool') }}
|
||||
</span>
|
||||
</div>
|
||||
<template v-if="item.kind === 'document'">
|
||||
<div class="reference-item__document">
|
||||
<t-icon name="file" class="reference-item__doc-icon" />
|
||||
<div class="reference-item__document-main">
|
||||
<div class="reference-item__title-row">
|
||||
<h5 class="reference-item__title">{{ item.title }}</h5>
|
||||
<a
|
||||
v-if="item.knowledgeBaseId && !embeddedMode"
|
||||
class="reference-item__open"
|
||||
:href="getDocumentHref(item)"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
:aria-label="t('chat.navigateToDocument')"
|
||||
@click.stop
|
||||
>
|
||||
<t-icon name="jump" size="14px" />
|
||||
</a>
|
||||
</div>
|
||||
<p v-if="item.snippet && !expandedKeys.has(item.key)" class="reference-item__snippet">
|
||||
{{ formatReferenceSnippet(item.snippet) }}
|
||||
</p>
|
||||
<div v-if="expandedKeys.has(item.key)" class="reference-item__content">
|
||||
{{ formatReferenceSnippet(item.content) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div v-if="item.kind === 'web' && item.domain" class="reference-item__source">
|
||||
<img
|
||||
v-if="item.faviconUrl"
|
||||
class="reference-item__source-mark"
|
||||
:src="item.faviconUrl"
|
||||
alt=""
|
||||
loading="lazy"
|
||||
@error="onFaviconError"
|
||||
/>
|
||||
<span class="reference-item__domain">{{ item.domain }}</span>
|
||||
</div>
|
||||
<div v-else-if="item.kind === 'tool' && item.domain" class="reference-item__source">
|
||||
<t-icon name="tools" class="reference-item__source-mark" />
|
||||
<span class="reference-item__domain">{{ item.domain }}</span>
|
||||
</div>
|
||||
|
||||
<h5 class="reference-card__title">{{ item.title }}</h5>
|
||||
<p v-if="item.snippet && !expandedKeys.has(item.key)" class="reference-card__snippet">
|
||||
{{ item.snippet }}
|
||||
</p>
|
||||
<div v-if="item.kind === 'document' && expandedKeys.has(item.key)" class="reference-card__content">
|
||||
{{ item.content }}
|
||||
</div>
|
||||
<div v-else-if="item.kind === 'tool' && item.content" class="reference-card__content">
|
||||
{{ item.content }}
|
||||
</div>
|
||||
<h5 v-if="shouldShowItemTitle(item)" class="reference-item__title">{{ item.title }}</h5>
|
||||
|
||||
<p v-if="item.snippet && !expandedKeys.has(item.key)" class="reference-item__snippet">
|
||||
{{ formatReferenceSnippet(item.snippet) }}
|
||||
</p>
|
||||
<div v-if="item.kind === 'tool' && item.content" class="reference-item__content">
|
||||
{{ formatReferenceSnippet(item.content) }}
|
||||
</div>
|
||||
</template>
|
||||
</component>
|
||||
|
||||
<div v-if="item.kind === 'document'" class="reference-card__actions">
|
||||
<a
|
||||
v-if="item.knowledgeBaseId && !embeddedMode"
|
||||
class="reference-card__action"
|
||||
:href="getDocumentHref(item)"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<t-icon name="jump" size="14px" />
|
||||
<span>{{ t('chat.navigateToDocument') }}</span>
|
||||
</a>
|
||||
</div>
|
||||
</article>
|
||||
</section>
|
||||
</div>
|
||||
@@ -140,6 +141,7 @@ import { useRouter } from 'vue-router'
|
||||
import { useChatReferencesDrawer } from '@/composables/useChatReferencesDrawer'
|
||||
import {
|
||||
buildReferenceSections,
|
||||
formatReferenceSnippet,
|
||||
resolveReferenceHighlightKey,
|
||||
type ReferenceListItem,
|
||||
} from '@/utils/referenceSources'
|
||||
@@ -267,6 +269,13 @@ function getDocumentHref(item: ReferenceListItem) {
|
||||
}).href
|
||||
}
|
||||
|
||||
function shouldShowItemTitle(item: ReferenceListItem) {
|
||||
if (item.kind !== 'web') return true
|
||||
const title = item.title?.trim()
|
||||
const domain = item.domain?.trim()
|
||||
return Boolean(title && title !== domain)
|
||||
}
|
||||
|
||||
async function scrollToHighlight() {
|
||||
if (!panelEntered.value) return
|
||||
const key = activeHighlightKey.value
|
||||
@@ -361,49 +370,45 @@ watch(visible, (open) => {
|
||||
|
||||
.chat-references-panel__title {
|
||||
margin: 0;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: var(--td-text-color-primary);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--td-text-color-secondary);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.chat-references-panel__count {
|
||||
flex-shrink: 0;
|
||||
min-width: 22px;
|
||||
height: 22px;
|
||||
padding: 0 8px;
|
||||
border-radius: 999px;
|
||||
background: var(--td-bg-color-secondarycontainer);
|
||||
color: var(--td-text-color-secondary);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--td-text-color-placeholder);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.chat-references-panel__close {
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: var(--td-text-color-placeholder);
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 8px;
|
||||
background: var(--td-bg-color-secondarycontainer);
|
||||
color: var(--td-text-color-secondary);
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
transition: background 0.15s ease, color 0.15s ease;
|
||||
|
||||
:deep(.t-icon) {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: var(--td-bg-color-component-hover);
|
||||
color: var(--td-text-color-secondary);
|
||||
background: color-mix(in srgb, var(--td-text-color-primary) 8%, var(--td-bg-color-secondarycontainer));
|
||||
color: var(--td-text-color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.chat-references-panel__body {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 12px 12px 20px;
|
||||
padding: 4px 12px 24px;
|
||||
}
|
||||
|
||||
.chat-references-panel__empty {
|
||||
@@ -413,6 +418,12 @@ watch(visible, (open) => {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.chat-references-panel__section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.chat-references-panel__section + .chat-references-panel__section {
|
||||
margin-top: 16px;
|
||||
}
|
||||
@@ -427,162 +438,139 @@ watch(visible, (open) => {
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
.reference-card {
|
||||
border: 1px solid var(--td-component-stroke);
|
||||
.reference-item {
|
||||
border-radius: 12px;
|
||||
background: var(--td-bg-color-container);
|
||||
overflow: hidden;
|
||||
transition: border-color 0.2s ease, box-shadow 0.2s ease, background-color 0.2s ease;
|
||||
transition: background-color 0.15s ease;
|
||||
|
||||
& + & {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: color-mix(in srgb, var(--td-text-color-placeholder) 42%, var(--td-component-stroke));
|
||||
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.04);
|
||||
&:hover:not(.is-highlighted) {
|
||||
background: color-mix(in srgb, var(--td-text-color-primary) 4%, transparent);
|
||||
}
|
||||
|
||||
&.is-highlighted {
|
||||
border-color: var(--td-text-color-placeholder);
|
||||
background: var(--td-bg-color-secondarycontainer);
|
||||
box-shadow: none;
|
||||
animation: reference-card-highlight 1s ease;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes reference-card-highlight {
|
||||
0% {
|
||||
background: var(--td-bg-color-component-hover);
|
||||
}
|
||||
100% {
|
||||
background: var(--td-bg-color-secondarycontainer);
|
||||
}
|
||||
}
|
||||
|
||||
.reference-card__inner {
|
||||
.reference-item__body {
|
||||
display: block;
|
||||
padding: 12px 14px;
|
||||
padding: 10px 12px;
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
transition: background-color 0.16s ease;
|
||||
|
||||
&.is-expandable {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.reference-card--web .reference-card__inner:hover {
|
||||
background: color-mix(in srgb, var(--td-text-color-primary) 3%, var(--td-bg-color-container));
|
||||
}
|
||||
|
||||
.reference-card--document .reference-card__inner:hover,
|
||||
.reference-card--tool .reference-card__inner:hover {
|
||||
background: color-mix(in srgb, var(--td-text-color-primary) 3%, var(--td-bg-color-container));
|
||||
}
|
||||
|
||||
.reference-card__meta {
|
||||
.reference-item__document {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.reference-card__index {
|
||||
.reference-item__doc-icon {
|
||||
flex-shrink: 0;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 999px;
|
||||
background: var(--td-bg-color-secondarycontainer);
|
||||
color: var(--td-text-color-secondary);
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 18px;
|
||||
margin-top: 3px;
|
||||
font-size: 16px;
|
||||
color: var(--td-text-color-primary);
|
||||
}
|
||||
|
||||
.reference-card__favicon {
|
||||
.reference-item__document-main {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.reference-item__source {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.reference-item__source-mark {
|
||||
flex-shrink: 0;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 4px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.reference-card__doc-icon {
|
||||
border-radius: 999px;
|
||||
object-fit: cover;
|
||||
font-size: 14px;
|
||||
color: var(--td-text-color-placeholder);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.reference-card__domain {
|
||||
font-size: 12px;
|
||||
.reference-item__domain {
|
||||
font-size: 13px;
|
||||
line-height: 1.35;
|
||||
color: var(--td-text-color-placeholder);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.reference-card__title {
|
||||
.reference-item__title-row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.reference-item__title {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
line-height: 1.45;
|
||||
line-height: 1.4;
|
||||
color: var(--td-text-color-primary);
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.reference-item__open {
|
||||
flex-shrink: 0;
|
||||
margin-top: 3px;
|
||||
color: var(--td-text-color-placeholder);
|
||||
line-height: 1;
|
||||
opacity: 0;
|
||||
transition: opacity 0.15s ease, color 0.15s ease;
|
||||
}
|
||||
|
||||
.reference-item:hover .reference-item__open,
|
||||
.reference-item.is-highlighted .reference-item__open {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.reference-item__open:hover {
|
||||
color: var(--td-text-color-primary);
|
||||
}
|
||||
|
||||
.reference-item__snippet {
|
||||
margin: 4px 0 0;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
color: var(--td-text-color-secondary);
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.reference-card__snippet {
|
||||
margin: 8px 0 0;
|
||||
.reference-item__content {
|
||||
margin: 4px 0 0;
|
||||
font-size: 13px;
|
||||
line-height: 1.55;
|
||||
color: var(--td-text-color-secondary);
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 4;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.reference-card__content {
|
||||
margin-top: 8px;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
color: var(--td-text-color-secondary);
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
max-height: 360px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.reference-card__actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
padding: 0 14px 12px;
|
||||
}
|
||||
|
||||
.reference-card__action {
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: var(--td-text-color-secondary);
|
||||
font-size: 12px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
color: var(--td-text-color-primary);
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.references-panel-enter-active {
|
||||
transition:
|
||||
transform 0.24s cubic-bezier(0.22, 0.61, 0.36, 1),
|
||||
|
||||
@@ -63,6 +63,8 @@ const dismiss = () => {
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
@import (reference) '../css/suggested-questions.less';
|
||||
|
||||
.follow-ups {
|
||||
max-width: 760px;
|
||||
margin: -4px 0 28px 46px;
|
||||
@@ -130,11 +132,9 @@ const dismiss = () => {
|
||||
transition: border-color .2s ease, box-shadow .2s ease, background .2s ease;
|
||||
}
|
||||
.follow-ups__item:hover {
|
||||
border-color: var(--td-brand-color);
|
||||
background: var(--td-bg-color-container-hover);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||
.suggestion-chip-hover();
|
||||
}
|
||||
.follow-ups__item:hover .t-icon { color: var(--td-brand-color); }
|
||||
.follow-ups__item:hover .t-icon { color: var(--td-text-color-secondary); }
|
||||
.is-spinning { animation: spin 1s linear infinite; }
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
@suggested-ease: cubic-bezier(0.16, 1, 0.3, 1);
|
||||
|
||||
// Shared hover for starter / follow-up suggestion chips.
|
||||
.suggestion-chip-hover() {
|
||||
border-color: color-mix(in srgb, var(--td-text-color-primary) 10%, var(--td-component-stroke));
|
||||
background: color-mix(in srgb, var(--td-text-color-primary) 4%, var(--td-bg-color-container));
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.suggestion-chip-active() {
|
||||
background: color-mix(in srgb, var(--td-text-color-primary) 6%, var(--td-bg-color-container));
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.suggested-questions-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -118,13 +130,11 @@
|
||||
transition: border-color 0.2s ease, box-shadow 0.2s ease, background 0.2s ease;
|
||||
|
||||
&:not(.sq-card-skeleton):hover {
|
||||
border-color: var(--td-brand-color);
|
||||
background: var(--td-bg-color-container-hover);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||
.suggestion-chip-hover();
|
||||
}
|
||||
|
||||
&:not(.sq-card-skeleton):active {
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);
|
||||
.suggestion-chip-active();
|
||||
}
|
||||
|
||||
&.sq-card-skeleton {
|
||||
|
||||
@@ -128,6 +128,53 @@ test('normalizeReferenceUrl trims trailing slash', () => {
|
||||
)
|
||||
})
|
||||
|
||||
test('buildReferenceList uses domain instead of raw url title', () => {
|
||||
const items = buildReferenceList([
|
||||
{
|
||||
id: 'http://bj.bendibao.com/xiuxian/202671/384250.shtm',
|
||||
chunk_type: 'web_search',
|
||||
knowledge_title: 'http://bj.bendibao.com/xiuxian/202671/384250.shtm',
|
||||
metadata: {
|
||||
url: 'http://bj.bendibao.com/xiuxian/202671/384250.shtm',
|
||||
snippet: '根据提供的网页内容...',
|
||||
},
|
||||
content: '根据提供的网页内容...',
|
||||
},
|
||||
])
|
||||
|
||||
assert.equal(items[0].title, 'bj.bendibao.com')
|
||||
assert.equal(items[0].domain, 'bj.bendibao.com')
|
||||
})
|
||||
|
||||
test('buildReferenceList prefers metadata title for web references', () => {
|
||||
const items = buildReferenceList([
|
||||
{
|
||||
id: 'https://example.com/post',
|
||||
chunk_type: 'web_search',
|
||||
knowledge_title: 'https://example.com/post',
|
||||
metadata: {
|
||||
url: 'https://example.com/post',
|
||||
title: 'Example headline',
|
||||
snippet: 'snippet text',
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
assert.equal(items[0].title, 'Example headline')
|
||||
})
|
||||
|
||||
test('formatReferenceSnippet strips markdown noise from preview text', async () => {
|
||||
const { formatReferenceSnippet } = await import('./referenceSources.ts')
|
||||
assert.equal(
|
||||
formatReferenceSnippet('... [Free Online Lectures](https://example.com) **Everything I Know**'),
|
||||
'Free Online Lectures Everything I Know',
|
||||
)
|
||||
assert.equal(
|
||||
formatReferenceSnippet(' Summary text'),
|
||||
'Summary text',
|
||||
)
|
||||
})
|
||||
|
||||
test('getDomainFromUrl strips www prefix', () => {
|
||||
assert.equal(getDomainFromUrl('https://www.example.com/x'), 'example.com')
|
||||
})
|
||||
|
||||
@@ -77,11 +77,25 @@ export function getFaviconUrl(urlOrDomain: string): string {
|
||||
}
|
||||
|
||||
function truncateText(text: string, maxLen: number): string {
|
||||
const normalized = String(text || '').replace(/\s+/g, ' ').trim()
|
||||
const normalized = formatReferenceSnippet(text)
|
||||
if (normalized.length <= maxLen) return normalized
|
||||
return `${normalized.slice(0, maxLen)}…`
|
||||
}
|
||||
|
||||
export function formatReferenceSnippet(text: string | undefined): string {
|
||||
let value = String(text || '').replace(/\s+/g, ' ').trim()
|
||||
if (!value) return ''
|
||||
value = value.replace(/^(\.{3}|…+)\s*/, '')
|
||||
value = value.replace(/!\[[^\]]*]\([^)]*\)/g, ' ')
|
||||
value = value.replace(/\[([^\]]+)]\([^)]*\)/g, '$1')
|
||||
value = value.replace(/`([^`]+)`/g, '$1')
|
||||
value = value.replace(/\*\*([^*]+)\*\*/g, '$1')
|
||||
value = value.replace(/\*([^*]+)\*/g, '$1')
|
||||
value = value.replace(/(^|\s)#+\s*/g, '$1')
|
||||
value = value.replace(/\s+/g, ' ').trim()
|
||||
return value
|
||||
}
|
||||
|
||||
function isWebReference(item: KnowledgeReferenceLike): boolean {
|
||||
return item.chunk_type === 'web_search'
|
||||
}
|
||||
@@ -90,18 +104,45 @@ function isToolReference(item: KnowledgeReferenceLike): boolean {
|
||||
return item.chunk_type === 'tool_result'
|
||||
}
|
||||
|
||||
function isLikelyUrl(text: string): boolean {
|
||||
const trimmed = String(text || '').trim()
|
||||
if (!trimmed) return false
|
||||
return /^https?:\/\//i.test(trimmed) || /^www\./i.test(trimmed)
|
||||
}
|
||||
|
||||
function isSameReferenceUrl(a: string, b: string): boolean {
|
||||
const left = String(a || '').trim()
|
||||
const right = String(b || '').trim()
|
||||
if (!left || !right) return false
|
||||
if (left === right) return true
|
||||
return normalizeReferenceUrl(left) === normalizeReferenceUrl(right)
|
||||
}
|
||||
|
||||
function resolveWebTitle(item: KnowledgeReferenceLike, url: string, domain: string): string {
|
||||
const metaTitle = item.metadata?.title?.trim()
|
||||
if (metaTitle && !isLikelyUrl(metaTitle)) return metaTitle
|
||||
|
||||
const knowledgeTitle = item.knowledge_title?.trim()
|
||||
if (
|
||||
knowledgeTitle &&
|
||||
!isLikelyUrl(knowledgeTitle) &&
|
||||
!isSameReferenceUrl(knowledgeTitle, url) &&
|
||||
knowledgeTitle !== domain
|
||||
) {
|
||||
return knowledgeTitle
|
||||
}
|
||||
|
||||
return domain || 'Web page'
|
||||
}
|
||||
|
||||
function buildWebItem(item: KnowledgeReferenceLike, index: number): ReferenceListItem | null {
|
||||
const url = getWebSearchUrl(item)
|
||||
if (!url) return null
|
||||
const title =
|
||||
item.knowledge_title ||
|
||||
item.metadata?.title ||
|
||||
getDomainFromUrl(url) ||
|
||||
url
|
||||
const domain = getDomainFromUrl(url)
|
||||
const title = resolveWebTitle(item, url, domain)
|
||||
const snippet =
|
||||
item.metadata?.snippet ||
|
||||
truncateText(item.content || '', 220)
|
||||
const domain = getDomainFromUrl(url)
|
||||
const normalizedUrl = normalizeReferenceUrl(url)
|
||||
return {
|
||||
key: `web:${normalizedUrl}`,
|
||||
|
||||
@@ -447,9 +447,9 @@ watch(
|
||||
transition: border-color 0.15s ease, box-shadow 0.15s ease, background 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--td-brand-color);
|
||||
background: var(--td-bg-color-container-hover);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||
border-color: color-mix(in srgb, var(--td-text-color-primary) 10%, var(--td-component-stroke));
|
||||
background: color-mix(in srgb, var(--td-text-color-primary) 4%, var(--td-bg-color-container));
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
&--skeleton {
|
||||
|
||||
Reference in New Issue
Block a user