2026-04-12 09:56:18 +08:00
|
|
|
|
<script setup lang="ts">
|
2026-05-01 20:39:09 +08:00
|
|
|
|
import { ArrowLeftOutlined, CopyOutlined } from '@ant-design/icons-vue'
|
2026-04-12 09:56:18 +08:00
|
|
|
|
import type {
|
|
|
|
|
|
MonitoringQuestionCitationAnalysisItem,
|
|
|
|
|
|
MonitoringQuestionDetailCitation,
|
|
|
|
|
|
MonitoringQuestionDetailPlatform,
|
2026-04-23 09:11:53 +08:00
|
|
|
|
MonitoringSourceItem,
|
2026-05-01 20:39:09 +08:00
|
|
|
|
} from '@geo/shared-types'
|
|
|
|
|
|
import { useQuery } from '@tanstack/vue-query'
|
|
|
|
|
|
import dayjs from 'dayjs'
|
|
|
|
|
|
import { computed, nextTick, onBeforeUnmount, ref, watch } from 'vue'
|
|
|
|
|
|
import { useI18n } from 'vue-i18n'
|
|
|
|
|
|
import { useRoute, useRouter } from 'vue-router'
|
|
|
|
|
|
|
|
|
|
|
|
import MarkdownPreview from '@/components/MarkdownPreview.vue'
|
|
|
|
|
|
import { monitoringApi } from '@/lib/api'
|
|
|
|
|
|
import { formatDateTime } from '@/lib/display'
|
|
|
|
|
|
import { formatError } from '@/lib/errors'
|
|
|
|
|
|
import { isMonitoringPlatformId, normalizeMonitoringPlatformId } from '@/lib/monitoring-platforms'
|
2026-05-20 15:37:25 +08:00
|
|
|
|
import { useCompanyStore } from '@/stores/company'
|
2026-05-01 20:39:09 +08:00
|
|
|
|
|
|
|
|
|
|
const { t } = useI18n()
|
|
|
|
|
|
const route = useRoute()
|
|
|
|
|
|
const router = useRouter()
|
2026-05-20 15:37:25 +08:00
|
|
|
|
const companyStore = useCompanyStore()
|
2026-05-01 20:39:09 +08:00
|
|
|
|
|
|
|
|
|
|
const trackingMaxHistoryDays = 30
|
|
|
|
|
|
const trackingToday = dayjs().format('YYYY-MM-DD')
|
|
|
|
|
|
|
2026-05-20 15:37:25 +08:00
|
|
|
|
const routeBrandId = computed(() => parsePositiveNumber(route.params.brandId))
|
|
|
|
|
|
const brandId = computed(() => companyStore.currentBrandId ?? routeBrandId.value)
|
2026-05-01 20:39:09 +08:00
|
|
|
|
const questionId = computed(() => parsePositiveNumber(route.params.questionId))
|
2026-05-24 00:27:32 +08:00
|
|
|
|
const questionHash = computed(
|
|
|
|
|
|
() =>
|
|
|
|
|
|
normalizeHistoryStateValue(history.state?.tracking_question_hash) ||
|
|
|
|
|
|
normalizeQueryValue(route.query.question_hash),
|
|
|
|
|
|
)
|
|
|
|
|
|
const questionTitleFallback = computed(
|
|
|
|
|
|
() =>
|
|
|
|
|
|
normalizeHistoryStateValue(history.state?.tracking_question_text) ||
|
|
|
|
|
|
normalizeQueryValue(route.query.question_text),
|
|
|
|
|
|
)
|
2026-05-01 20:39:09 +08:00
|
|
|
|
const keywordId = computed(() => normalizeQueryValue(route.query.keyword_id))
|
2026-04-23 09:11:53 +08:00
|
|
|
|
const requestedPlatformId = computed(() => {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
const platformId = normalizeMonitoringPlatformId(normalizeQueryValue(route.query.ai_platform_id))
|
|
|
|
|
|
return isMonitoringPlatformId(platformId) ? platformId : ''
|
|
|
|
|
|
})
|
|
|
|
|
|
const businessDate = computed(
|
|
|
|
|
|
() => normalizeTrackingBusinessDate(route.query.business_date) || trackingToday,
|
|
|
|
|
|
)
|
2026-04-12 09:56:18 +08:00
|
|
|
|
const dateFrom = computed(() => {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
return normalizeQueryValue(route.query.date_from) || businessDate.value
|
|
|
|
|
|
})
|
2026-04-12 09:56:18 +08:00
|
|
|
|
const dateTo = computed(() => {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
return normalizeQueryValue(route.query.date_to) || businessDate.value
|
|
|
|
|
|
})
|
2026-04-12 09:56:18 +08:00
|
|
|
|
|
|
|
|
|
|
const detailQuery = useQuery({
|
|
|
|
|
|
queryKey: computed(() => [
|
2026-05-01 20:39:09 +08:00
|
|
|
|
'tracking',
|
|
|
|
|
|
'question-detail-page',
|
2026-04-12 09:56:18 +08:00
|
|
|
|
brandId.value,
|
|
|
|
|
|
questionId.value,
|
|
|
|
|
|
questionHash.value,
|
2026-04-13 16:08:12 +08:00
|
|
|
|
requestedPlatformId.value,
|
2026-04-12 09:56:18 +08:00
|
|
|
|
dateFrom.value,
|
|
|
|
|
|
dateTo.value,
|
|
|
|
|
|
]),
|
|
|
|
|
|
enabled: computed(() => Boolean(brandId.value && questionId.value)),
|
|
|
|
|
|
queryFn: () =>
|
|
|
|
|
|
monitoringApi.questionDetail(brandId.value as number, questionId.value as number, {
|
|
|
|
|
|
date_from: dateFrom.value,
|
|
|
|
|
|
date_to: dateTo.value,
|
|
|
|
|
|
question_hash: questionHash.value || undefined,
|
2026-04-13 16:08:12 +08:00
|
|
|
|
ai_platform_id: requestedPlatformId.value || undefined,
|
2026-04-12 09:56:18 +08:00
|
|
|
|
}),
|
2026-05-01 20:39:09 +08:00
|
|
|
|
})
|
2026-04-12 09:56:18 +08:00
|
|
|
|
|
2026-05-01 20:39:09 +08:00
|
|
|
|
const activePlatformId = ref('')
|
|
|
|
|
|
const detailPlatforms = computed<MonitoringQuestionDetailPlatform[]>(
|
|
|
|
|
|
() => detailQuery.data.value?.platforms ?? [],
|
|
|
|
|
|
)
|
2026-04-24 22:21:01 +08:00
|
|
|
|
const hasNoDetailPlatforms = computed(() => {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
return Boolean(detailQuery.data.value) && detailPlatforms.value.length === 0
|
|
|
|
|
|
})
|
2026-04-24 22:21:01 +08:00
|
|
|
|
const shouldShowDetailUnavailableNotice = computed(() => {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
return hasNoDetailPlatforms.value
|
|
|
|
|
|
})
|
|
|
|
|
|
const detailUnavailableNoticeType = computed<'warning' | 'info'>(() => {
|
|
|
|
|
|
return 'info'
|
|
|
|
|
|
})
|
2026-04-24 22:21:01 +08:00
|
|
|
|
const detailUnavailableNoticeTitle = computed(() => {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
return requestedPlatformId.value
|
|
|
|
|
|
? t('tracking.selectedPlatformUnauthorizedTitle')
|
|
|
|
|
|
: t('tracking.noDetailPlatformsTitle')
|
|
|
|
|
|
})
|
2026-04-24 22:21:01 +08:00
|
|
|
|
const detailUnavailableNoticeDescription = computed(() => {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
return requestedPlatformId.value
|
|
|
|
|
|
? t('tracking.selectedPlatformUnauthorizedDescription')
|
|
|
|
|
|
: t('tracking.noDetailPlatformsDescription')
|
|
|
|
|
|
})
|
|
|
|
|
|
const detailCitationAnalysis = computed<MonitoringQuestionCitationAnalysisItem[]>(
|
|
|
|
|
|
() => detailQuery.data.value?.citation_analysis ?? [],
|
|
|
|
|
|
)
|
|
|
|
|
|
const citationSourcesScrollRef = ref<HTMLElement | null>(null)
|
|
|
|
|
|
const highlightedCitationIndex = ref<number | null>(null)
|
|
|
|
|
|
|
|
|
|
|
|
let citationHighlightTimer: ReturnType<typeof setTimeout> | null = null
|
2026-04-12 09:56:18 +08:00
|
|
|
|
|
|
|
|
|
|
watch(
|
2026-04-23 09:11:53 +08:00
|
|
|
|
detailPlatforms,
|
2026-04-12 09:56:18 +08:00
|
|
|
|
(platforms) => {
|
|
|
|
|
|
if (!platforms?.length) {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
activePlatformId.value = ''
|
|
|
|
|
|
return
|
2026-04-12 09:56:18 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-13 16:08:12 +08:00
|
|
|
|
const requestedPlatform = requestedPlatformId.value
|
|
|
|
|
|
? platforms.find((item) => item.ai_platform_id === requestedPlatformId.value)
|
2026-05-01 20:39:09 +08:00
|
|
|
|
: null
|
|
|
|
|
|
const sampledPlatform = platforms.find((item) => item.sample_status === 'sampled')
|
|
|
|
|
|
const nextPlatform = requestedPlatform ?? sampledPlatform ?? platforms[0]
|
2026-04-12 09:56:18 +08:00
|
|
|
|
|
|
|
|
|
|
if (!platforms.some((item) => item.ai_platform_id === activePlatformId.value)) {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
activePlatformId.value = nextPlatform.ai_platform_id
|
2026-04-12 09:56:18 +08:00
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
{ immediate: true },
|
2026-05-01 20:39:09 +08:00
|
|
|
|
)
|
2026-04-12 09:56:18 +08:00
|
|
|
|
|
|
|
|
|
|
const pageTitle = computed(() => {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
return (
|
|
|
|
|
|
detailQuery.data.value?.question_text ??
|
|
|
|
|
|
questionTitleFallback.value ??
|
|
|
|
|
|
t('tracking.questionDetailFallback')
|
|
|
|
|
|
)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const activePlatform = computed<MonitoringQuestionDetailPlatform | null>(
|
|
|
|
|
|
() =>
|
|
|
|
|
|
detailPlatforms.value.find((item) => item.ai_platform_id === activePlatformId.value) ?? null,
|
|
|
|
|
|
)
|
2026-04-12 09:56:18 +08:00
|
|
|
|
|
|
|
|
|
|
const activeCitationAnalysis = computed<MonitoringQuestionCitationAnalysisItem[]>(() => {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
const items = detailCitationAnalysis.value
|
2026-04-12 09:56:18 +08:00
|
|
|
|
if (!activePlatformId.value) {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
return items
|
2026-04-12 09:56:18 +08:00
|
|
|
|
}
|
2026-05-01 20:39:09 +08:00
|
|
|
|
return items.filter((item) => item.ai_platform_id === activePlatformId.value)
|
|
|
|
|
|
})
|
2026-04-12 09:56:18 +08:00
|
|
|
|
|
|
|
|
|
|
const latestSampledAt = computed<string | null>(() => {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
const platforms = detailPlatforms.value
|
2026-04-12 09:56:18 +08:00
|
|
|
|
const sampledAtValues = platforms
|
|
|
|
|
|
.map((item) => item.sampled_at)
|
2026-05-01 20:39:09 +08:00
|
|
|
|
.filter((value): value is string => Boolean(value))
|
2026-04-12 09:56:18 +08:00
|
|
|
|
|
|
|
|
|
|
if (!sampledAtValues.length) {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
return null
|
2026-04-12 09:56:18 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return sampledAtValues.reduce((latest, current) => {
|
|
|
|
|
|
if (!latest) {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
return current
|
2026-04-12 09:56:18 +08:00
|
|
|
|
}
|
2026-05-01 20:39:09 +08:00
|
|
|
|
return dayjs(current).isAfter(dayjs(latest)) ? current : latest
|
|
|
|
|
|
}, sampledAtValues[0] ?? null)
|
|
|
|
|
|
})
|
2026-04-12 09:56:18 +08:00
|
|
|
|
|
|
|
|
|
|
const collectedAtDisplay = computed(() => {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
const sampledAt = activePlatform.value?.sampled_at || latestSampledAt.value
|
|
|
|
|
|
return sampledAt ? formatDateTime(sampledAt) : '--'
|
|
|
|
|
|
})
|
2026-04-12 09:56:18 +08:00
|
|
|
|
|
2026-04-22 00:24:21 +08:00
|
|
|
|
type TrackingAnalyzedContentCitation = {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
key: string
|
|
|
|
|
|
contentName: string
|
|
|
|
|
|
channelName: string
|
|
|
|
|
|
citationCount: number
|
|
|
|
|
|
citationRate: number | null
|
|
|
|
|
|
citedURL: string | null
|
|
|
|
|
|
}
|
2026-04-22 00:24:21 +08:00
|
|
|
|
|
2026-04-23 22:54:56 +08:00
|
|
|
|
type TrackingCitationSourceLookupItem = {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
sourceGroupIndex: number
|
|
|
|
|
|
citation: MonitoringQuestionDetailCitation
|
|
|
|
|
|
}
|
2026-04-23 22:54:56 +08:00
|
|
|
|
|
2026-04-23 09:11:53 +08:00
|
|
|
|
const renderedAnswerContent = computed<string | null>(() => {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
return formatAnswerContent(activePlatform.value)
|
|
|
|
|
|
})
|
2026-04-22 00:24:21 +08:00
|
|
|
|
|
2026-04-23 22:54:56 +08:00
|
|
|
|
const activeDeepSeekCitationReferenceLabels = computed<string[][]>(() => {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
return collectDeepSeekCitationReferenceLabels(activePlatform.value)
|
|
|
|
|
|
})
|
2026-04-23 22:54:56 +08:00
|
|
|
|
|
2026-04-22 00:24:21 +08:00
|
|
|
|
const activeInlineContentCitations = computed<TrackingAnalyzedContentCitation[]>(() => {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
return analyzeInlineContentCitations(activePlatform.value)
|
|
|
|
|
|
})
|
2026-04-22 00:24:21 +08:00
|
|
|
|
|
|
|
|
|
|
const shouldShowInlineContentCitations = computed(() => {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
return activeInlineContentCitations.value.length > 0
|
|
|
|
|
|
})
|
2026-04-22 00:24:21 +08:00
|
|
|
|
|
2026-04-23 09:11:53 +08:00
|
|
|
|
watch(activePlatformId, () => {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
clearCitationHighlight()
|
|
|
|
|
|
})
|
2026-04-23 09:11:53 +08:00
|
|
|
|
|
|
|
|
|
|
onBeforeUnmount(() => {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
clearCitationHighlight()
|
|
|
|
|
|
})
|
2026-04-23 09:11:53 +08:00
|
|
|
|
|
2026-04-12 09:56:18 +08:00
|
|
|
|
function goBack(): void {
|
|
|
|
|
|
void router.push({
|
2026-05-01 20:39:09 +08:00
|
|
|
|
name: 'tracking',
|
2026-04-12 09:56:18 +08:00
|
|
|
|
query: {
|
|
|
|
|
|
brand_id: brandId.value ? String(brandId.value) : undefined,
|
|
|
|
|
|
keyword_id: keywordId.value || undefined,
|
2026-04-13 16:08:12 +08:00
|
|
|
|
ai_platform_id: requestedPlatformId.value || undefined,
|
2026-04-12 09:56:18 +08:00
|
|
|
|
business_date: businessDate.value,
|
|
|
|
|
|
},
|
2026-05-01 20:39:09 +08:00
|
|
|
|
})
|
2026-04-12 09:56:18 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-25 21:36:44 +08:00
|
|
|
|
function openExternalURL(url: string | null | undefined): void {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
const normalized = String(url ?? '').trim()
|
2026-04-25 21:36:44 +08:00
|
|
|
|
if (!normalized) {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
return
|
2026-04-25 21:36:44 +08:00
|
|
|
|
}
|
2026-05-01 20:39:09 +08:00
|
|
|
|
window.open(normalized, '_blank', 'noopener,noreferrer')
|
2026-04-25 21:36:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-01 20:39:09 +08:00
|
|
|
|
function openImitationCreate(
|
|
|
|
|
|
sourceURL: string | null | undefined,
|
|
|
|
|
|
sourceTitle?: string | null,
|
|
|
|
|
|
): void {
|
|
|
|
|
|
const normalizedURL = String(sourceURL ?? '').trim()
|
2026-04-25 21:36:44 +08:00
|
|
|
|
if (!normalizedURL) {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
return
|
2026-04-25 21:36:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void router.push({
|
2026-05-01 20:39:09 +08:00
|
|
|
|
name: 'article-imitation-create',
|
2026-04-25 21:36:44 +08:00
|
|
|
|
query: {
|
|
|
|
|
|
source_url: normalizedURL,
|
2026-05-01 20:39:09 +08:00
|
|
|
|
source_title: String(sourceTitle ?? '').trim() || undefined,
|
2026-04-25 21:36:44 +08:00
|
|
|
|
},
|
2026-05-01 20:39:09 +08:00
|
|
|
|
})
|
2026-04-25 21:36:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-12 09:56:18 +08:00
|
|
|
|
function formatPercent(value: number | null | undefined): string {
|
|
|
|
|
|
if (value === null || value === undefined) {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
return '--'
|
2026-04-12 09:56:18 +08:00
|
|
|
|
}
|
2026-05-01 20:39:09 +08:00
|
|
|
|
return `${(value * 100).toFixed(2)}%`
|
2026-04-12 09:56:18 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function statusTagColor(status: string): string {
|
|
|
|
|
|
switch (status) {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
case 'sampled':
|
|
|
|
|
|
return 'green'
|
|
|
|
|
|
case 'pending':
|
|
|
|
|
|
return 'gold'
|
|
|
|
|
|
case 'not_logged_in':
|
|
|
|
|
|
return 'default'
|
|
|
|
|
|
case 'unavailable':
|
|
|
|
|
|
return 'red'
|
2026-04-12 09:56:18 +08:00
|
|
|
|
default:
|
2026-05-01 20:39:09 +08:00
|
|
|
|
return 'default'
|
2026-04-12 09:56:18 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function statusLabel(status: string): string {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
return t(`tracking.status.${status}`)
|
2026-04-12 09:56:18 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-24 23:37:16 +08:00
|
|
|
|
function resolveDeepSeekProviderModelLabel(value: string | null | undefined): string {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
const normalized = String(value ?? '').trim()
|
|
|
|
|
|
if (!normalized || normalized === '--') {
|
|
|
|
|
|
return 'DeepSeek'
|
2026-04-24 23:37:16 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-01 20:39:09 +08:00
|
|
|
|
const compact = normalized.replace(/\s+/g, '').toLowerCase()
|
2026-04-24 23:37:16 +08:00
|
|
|
|
if (/r1|reasoner|deepthink|深度思考/.test(compact)) {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
return 'DeepSeek'
|
2026-04-24 23:37:16 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const versionMatch =
|
2026-05-01 20:39:09 +08:00
|
|
|
|
normalized.match(/deepseek[-_\s]*(r1|v\d+(?:\.\d+)?)/i) ??
|
|
|
|
|
|
normalized.match(/\b(r1|v\d+(?:\.\d+)?)\b/i)
|
2026-04-24 23:37:16 +08:00
|
|
|
|
if (versionMatch?.[1]) {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
return `DeepSeek-${versionMatch[1].toUpperCase()}`
|
2026-04-24 23:37:16 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (/search|web|联网|搜索/.test(compact)) {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
return 'DeepSeek'
|
2026-04-24 23:37:16 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-01 20:39:09 +08:00
|
|
|
|
return normalized
|
2026-04-24 23:37:16 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function resolveAnswerProviderModel(platform: MonitoringQuestionDetailPlatform | null): string {
|
|
|
|
|
|
if (isDeepSeekInlineCitationPlatform(platform?.ai_platform_id)) {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
return resolveDeepSeekProviderModelLabel(platform?.provider_model)
|
2026-04-24 23:37:16 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-01 20:39:09 +08:00
|
|
|
|
return String(platform?.provider_model ?? '').trim() || '--'
|
2026-04-24 23:37:16 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-12 09:56:18 +08:00
|
|
|
|
function resolveCitationTitle(citation: MonitoringQuestionDetailCitation): string {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
return citation.cited_title || citation.article_title || citation.cited_url
|
2026-04-12 09:56:18 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-23 09:11:53 +08:00
|
|
|
|
function isLinkedInlineCitationPlatform(platformId: string | null | undefined): boolean {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
return platformId === 'yuanbao'
|
2026-04-23 09:11:53 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function isKimiInlineCitationPlatform(platformId: string | null | undefined): boolean {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
return platformId === 'kimi'
|
2026-04-23 09:11:53 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-23 22:54:56 +08:00
|
|
|
|
function isDeepSeekInlineCitationPlatform(platformId: string | null | undefined): boolean {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
return platformId === 'deepseek'
|
2026-04-23 22:54:56 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-25 16:48:28 +08:00
|
|
|
|
function supportsContentCitationDisplay(platformId: string | null | undefined): boolean {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
return platformId !== 'qwen'
|
2026-04-25 16:48:28 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-22 00:24:21 +08:00
|
|
|
|
function createQwenSourceGroupPattern(): RegExp {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
return /\[\[source_group_web_(\d+)\]\]/g
|
2026-04-22 00:24:21 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-23 09:11:53 +08:00
|
|
|
|
function createYuanbaoCitationPattern(): RegExp {
|
2026-05-08 21:51:24 +08:00
|
|
|
|
// Match both formats so the same renderer works across the migration:
|
|
|
|
|
|
// - "[citation:N]" — legacy yuanbao SSE protocol.
|
|
|
|
|
|
// - "[N]" — new hunyuan_t1 protocol; the desktop adapter now lowers
|
|
|
|
|
|
// `<div data-idx-list="1,2,10">` placeholders to bare "[N]" and also
|
|
|
|
|
|
// normalises any residual "[citation:N]" SSE frames to bare "[N]" so
|
|
|
|
|
|
// yuanbao answers render with the same chip-style superscript as
|
|
|
|
|
|
// DeepSeek.
|
|
|
|
|
|
return /\[(?:citation:\s*)?(\d+)\]/gi
|
2026-04-23 09:11:53 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function createYuanbaoCitationStripPattern(): RegExp {
|
2026-05-08 21:51:24 +08:00
|
|
|
|
// The pattern's `replace` step covers any `[N]` we successfully matched.
|
|
|
|
|
|
// Strip leftover legacy `[citation:...]` fragments only — we must not
|
|
|
|
|
|
// strip bare "[N]" tokens here, because formatAnswerContent runs strip
|
|
|
|
|
|
// *after* replacing matched markers, so any remaining "[N]" is incidental
|
|
|
|
|
|
// text that should stay untouched.
|
2026-05-01 20:39:09 +08:00
|
|
|
|
return /\[\s*citation\s*:[^\[\]]*?\]/gi
|
2026-04-23 09:11:53 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function createQwenSourceGroupStripPattern(): RegExp {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
return /\[\[\s*source_group_web_[^\[\]]*?\]\]/gi
|
2026-04-23 09:11:53 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function createMarkdownLinkPattern(): RegExp {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
return /\[([^\]\n]+)\]\((https?:\/\/[^)\s]+)\)/g
|
2026-04-23 09:11:53 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-23 22:54:56 +08:00
|
|
|
|
function looksLikeHtmlAnswer(value: string | null | undefined): boolean {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
const normalized = String(value ?? '').trim()
|
|
|
|
|
|
return /<\/?[a-z][^>]*>/i.test(normalized)
|
2026-04-23 22:54:56 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function parseHtmlFragment(content: string): HTMLElement | null {
|
|
|
|
|
|
if (!looksLikeHtmlAnswer(content)) {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
return null
|
2026-04-23 22:54:56 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-01 20:39:09 +08:00
|
|
|
|
const parser = new DOMParser()
|
|
|
|
|
|
const document = parser.parseFromString(`<div>${content}</div>`, 'text/html')
|
|
|
|
|
|
return document.body.firstElementChild instanceof HTMLElement
|
|
|
|
|
|
? document.body.firstElementChild
|
|
|
|
|
|
: null
|
2026-04-23 22:54:56 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-23 09:11:53 +08:00
|
|
|
|
function collectInlineCitationIndexes(answerText: string): number[] {
|
|
|
|
|
|
const matches = [
|
|
|
|
|
|
...answerText.matchAll(createQwenSourceGroupPattern()),
|
|
|
|
|
|
...answerText.matchAll(createYuanbaoCitationPattern()),
|
2026-05-01 20:39:09 +08:00
|
|
|
|
]
|
2026-04-23 09:11:53 +08:00
|
|
|
|
|
|
|
|
|
|
return matches
|
|
|
|
|
|
.map((match) => Number(match[1]))
|
2026-05-01 20:39:09 +08:00
|
|
|
|
.filter((value) => Number.isFinite(value) && value > 0)
|
2026-04-23 09:11:53 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function normalizeInlineCitationUrl(value: string | null | undefined): string {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
const normalized = String(value ?? '').trim()
|
2026-04-23 09:11:53 +08:00
|
|
|
|
if (!normalized) {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
return ''
|
2026-04-23 09:11:53 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
const url = new URL(normalized)
|
|
|
|
|
|
url.hash = ''
|
|
|
|
|
|
return url.toString()
|
2026-04-23 09:11:53 +08:00
|
|
|
|
} catch {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
return normalized
|
2026-04-23 09:11:53 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function deriveCitationChannelName(url: string): string {
|
|
|
|
|
|
try {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
return new URL(url).hostname || '--'
|
2026-04-23 09:11:53 +08:00
|
|
|
|
} catch {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
return '--'
|
2026-04-23 09:11:53 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function collectKimiInlineCitationOccurrences(answerText: string): Array<{
|
2026-05-01 20:39:09 +08:00
|
|
|
|
url: string
|
|
|
|
|
|
label: string | null
|
|
|
|
|
|
citationCount: number
|
2026-04-23 09:11:53 +08:00
|
|
|
|
}> {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
const counts = new Map<string, { label: string | null; citationCount: number }>()
|
|
|
|
|
|
const pattern = createMarkdownLinkPattern()
|
2026-04-23 09:11:53 +08:00
|
|
|
|
|
|
|
|
|
|
for (const match of answerText.matchAll(pattern)) {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
const matchIndex = match.index ?? 0
|
|
|
|
|
|
if (matchIndex > 0 && answerText[matchIndex - 1] === '!') {
|
|
|
|
|
|
continue
|
2026-04-23 09:11:53 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-01 20:39:09 +08:00
|
|
|
|
const label = String(match[1] ?? '').trim() || null
|
|
|
|
|
|
const url = normalizeInlineCitationUrl(match[2])
|
2026-04-23 09:11:53 +08:00
|
|
|
|
if (!url) {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
continue
|
2026-04-23 09:11:53 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-01 20:39:09 +08:00
|
|
|
|
const existing = counts.get(url)
|
2026-04-23 09:11:53 +08:00
|
|
|
|
if (existing) {
|
|
|
|
|
|
counts.set(url, {
|
|
|
|
|
|
label: existing.label ?? label,
|
|
|
|
|
|
citationCount: existing.citationCount + 1,
|
2026-05-01 20:39:09 +08:00
|
|
|
|
})
|
|
|
|
|
|
continue
|
2026-04-23 09:11:53 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
counts.set(url, {
|
|
|
|
|
|
label,
|
|
|
|
|
|
citationCount: 1,
|
2026-05-01 20:39:09 +08:00
|
|
|
|
})
|
2026-04-23 09:11:53 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return Array.from(counts.entries()).map(([url, value]) => ({
|
|
|
|
|
|
url,
|
|
|
|
|
|
label: value.label,
|
|
|
|
|
|
citationCount: value.citationCount,
|
2026-05-01 20:39:09 +08:00
|
|
|
|
}))
|
2026-04-23 09:11:53 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-01 20:39:09 +08:00
|
|
|
|
function buildInlineCitationSourceLookup(
|
|
|
|
|
|
platform: MonitoringQuestionDetailPlatform | null,
|
|
|
|
|
|
): Map<string, MonitoringSourceItem> {
|
|
|
|
|
|
const lookup = new Map<string, MonitoringSourceItem>()
|
2026-04-23 09:11:53 +08:00
|
|
|
|
for (const item of platform?.inline_citations ?? []) {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
const normalizedURL = normalizeInlineCitationUrl(item.normalized_url ?? item.url)
|
2026-04-23 09:11:53 +08:00
|
|
|
|
if (!normalizedURL || lookup.has(normalizedURL)) {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
continue
|
2026-04-23 09:11:53 +08:00
|
|
|
|
}
|
2026-05-01 20:39:09 +08:00
|
|
|
|
lookup.set(normalizedURL, item)
|
2026-04-23 09:11:53 +08:00
|
|
|
|
}
|
2026-05-01 20:39:09 +08:00
|
|
|
|
return lookup
|
2026-04-23 09:11:53 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-01 20:39:09 +08:00
|
|
|
|
function buildCitationSourceLookup(
|
|
|
|
|
|
platform: MonitoringQuestionDetailPlatform | null,
|
|
|
|
|
|
): Map<string, TrackingCitationSourceLookupItem> {
|
|
|
|
|
|
const lookup = new Map<string, TrackingCitationSourceLookupItem>()
|
2026-04-23 22:54:56 +08:00
|
|
|
|
for (const [index, citation] of (platform?.citations ?? []).entries()) {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
const normalizedURL = normalizeInlineCitationUrl(citation.cited_url)
|
2026-04-23 22:54:56 +08:00
|
|
|
|
if (!normalizedURL || lookup.has(normalizedURL)) {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
continue
|
2026-04-23 22:54:56 +08:00
|
|
|
|
}
|
|
|
|
|
|
lookup.set(normalizedURL, {
|
|
|
|
|
|
sourceGroupIndex: index + 1,
|
|
|
|
|
|
citation,
|
2026-05-01 20:39:09 +08:00
|
|
|
|
})
|
2026-04-23 22:54:56 +08:00
|
|
|
|
}
|
2026-05-01 20:39:09 +08:00
|
|
|
|
return lookup
|
2026-04-23 22:54:56 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-24 23:37:16 +08:00
|
|
|
|
function buildDeepSeekAnswerLinkSourceLookup(
|
|
|
|
|
|
platform: MonitoringQuestionDetailPlatform | null,
|
|
|
|
|
|
): Map<string, { title: string | null; siteName: string | null }> {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
const lookup = new Map<string, { title: string | null; siteName: string | null }>()
|
2026-04-24 23:37:16 +08:00
|
|
|
|
|
|
|
|
|
|
for (const item of platform?.inline_citations ?? []) {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
const normalizedURL = normalizeInlineCitationUrl(item.normalized_url ?? item.url)
|
2026-04-24 23:37:16 +08:00
|
|
|
|
if (!normalizedURL || lookup.has(normalizedURL)) {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
continue
|
2026-04-24 23:37:16 +08:00
|
|
|
|
}
|
|
|
|
|
|
lookup.set(normalizedURL, {
|
|
|
|
|
|
title: item.title?.trim() || null,
|
|
|
|
|
|
siteName: item.site_name?.trim() || null,
|
2026-05-01 20:39:09 +08:00
|
|
|
|
})
|
2026-04-24 23:37:16 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
for (const citation of platform?.citations ?? []) {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
const normalizedURL = normalizeInlineCitationUrl(citation.cited_url)
|
2026-04-24 23:37:16 +08:00
|
|
|
|
if (!normalizedURL || lookup.has(normalizedURL)) {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
continue
|
2026-04-24 23:37:16 +08:00
|
|
|
|
}
|
|
|
|
|
|
lookup.set(normalizedURL, {
|
|
|
|
|
|
title: resolveCitationTitle(citation),
|
|
|
|
|
|
siteName: citation.site_name?.trim() || null,
|
2026-05-01 20:39:09 +08:00
|
|
|
|
})
|
2026-04-24 23:37:16 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-01 20:39:09 +08:00
|
|
|
|
return lookup
|
2026-04-24 23:37:16 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function isHiddenDeepSeekAnswerNode(element: Element): boolean {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
let current: Element | null = element
|
2026-04-24 23:37:16 +08:00
|
|
|
|
for (let depth = 0; current && depth < 8; depth += 1) {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
if (current.hasAttribute('hidden') || current.getAttribute('aria-hidden') === 'true') {
|
|
|
|
|
|
return true
|
2026-04-24 23:37:16 +08:00
|
|
|
|
}
|
2026-05-01 20:39:09 +08:00
|
|
|
|
const style = current.getAttribute('style') ?? ''
|
2026-04-24 23:37:16 +08:00
|
|
|
|
if (/(display\s*:\s*none|visibility\s*:\s*hidden|opacity\s*:\s*0)/i.test(style)) {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
return true
|
2026-04-24 23:37:16 +08:00
|
|
|
|
}
|
2026-05-01 20:39:09 +08:00
|
|
|
|
current = current.parentElement
|
2026-04-24 23:37:16 +08:00
|
|
|
|
}
|
2026-05-01 20:39:09 +08:00
|
|
|
|
return false
|
2026-04-24 23:37:16 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function isInsideDeepSeekAuxiliaryLinkContainer(anchor: HTMLAnchorElement): boolean {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
let current = anchor.parentElement
|
2026-04-24 23:37:16 +08:00
|
|
|
|
for (let depth = 0; current && depth < 8; depth += 1) {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
const descriptor = `${current.className || ''} ${current.getAttribute('data-testid') || ''} ${current.getAttribute('role') || ''}`
|
|
|
|
|
|
if (
|
|
|
|
|
|
/(search-view-card|search-result|result-card|source-card|reference-card|citation-card|popover|tooltip|dropdown|floating|hover-card|preview-card)/i.test(
|
|
|
|
|
|
descriptor,
|
|
|
|
|
|
)
|
|
|
|
|
|
) {
|
|
|
|
|
|
return true
|
2026-04-24 23:37:16 +08:00
|
|
|
|
}
|
2026-05-01 20:39:09 +08:00
|
|
|
|
current = current.parentElement
|
2026-04-24 23:37:16 +08:00
|
|
|
|
}
|
2026-05-01 20:39:09 +08:00
|
|
|
|
return false
|
2026-04-24 23:37:16 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function shouldCountDeepSeekAnswerLink(anchor: HTMLAnchorElement): boolean {
|
|
|
|
|
|
if (isHiddenDeepSeekAnswerNode(anchor) || isInsideDeepSeekAuxiliaryLinkContainer(anchor)) {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
return false
|
2026-04-24 23:37:16 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-01 20:39:09 +08:00
|
|
|
|
const text = String(anchor.textContent ?? '').trim()
|
2026-04-24 23:37:16 +08:00
|
|
|
|
if (normalizeDeepSeekCitationLabel(text)) {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
return true
|
2026-04-24 23:37:16 +08:00
|
|
|
|
}
|
|
|
|
|
|
if (shouldStripDeepSeekOrphanAnchor(anchor)) {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
return false
|
2026-04-24 23:37:16 +08:00
|
|
|
|
}
|
2026-05-01 20:39:09 +08:00
|
|
|
|
return /[A-Za-z0-9\u3400-\u9fff]/.test(text)
|
2026-04-24 23:37:16 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-23 22:54:56 +08:00
|
|
|
|
function normalizeDeepSeekCitationLabel(value: string | null | undefined): string | null {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
const normalized = String(value ?? '')
|
|
|
|
|
|
.trim()
|
|
|
|
|
|
.replace(/\s+/g, ' ')
|
2026-04-23 22:54:56 +08:00
|
|
|
|
if (!normalized) {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
return null
|
2026-04-23 22:54:56 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-01 20:39:09 +08:00
|
|
|
|
const compact = normalized.replace(/\s+/g, '')
|
|
|
|
|
|
const numericCandidate =
|
|
|
|
|
|
compact.match(/^\[?(-?\d+)\]?$/)?.[1] ??
|
|
|
|
|
|
compact.match(/^[((【](-?\d+)[】))]$/)?.[1] ??
|
|
|
|
|
|
compact.match(/^#?(-?\d+)$/)?.[1] ??
|
|
|
|
|
|
null
|
2026-04-23 22:54:56 +08:00
|
|
|
|
if (numericCandidate) {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
return `[${numericCandidate.replace(/^-/, '')}]`
|
2026-04-23 22:54:56 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-01 20:39:09 +08:00
|
|
|
|
return null
|
2026-04-23 22:54:56 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function getOrCreateDeepSeekFallbackCitationLabel(
|
|
|
|
|
|
normalizedURL: string,
|
|
|
|
|
|
fallbackLabels: Map<string, string>,
|
|
|
|
|
|
state: { nextIndex: number },
|
|
|
|
|
|
): string {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
const existing = fallbackLabels.get(normalizedURL)
|
2026-04-23 22:54:56 +08:00
|
|
|
|
if (existing) {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
return existing
|
2026-04-23 22:54:56 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-01 20:39:09 +08:00
|
|
|
|
const nextLabel = `[${state.nextIndex}]`
|
|
|
|
|
|
fallbackLabels.set(normalizedURL, nextLabel)
|
|
|
|
|
|
state.nextIndex += 1
|
|
|
|
|
|
return nextLabel
|
2026-04-23 22:54:56 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function pushUniqueDeepSeekCitationLabel(labels: string[], label: string): void {
|
|
|
|
|
|
if (!labels.includes(label)) {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
labels.push(label)
|
2026-04-23 22:54:56 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function shouldStripDeepSeekOrphanAnchor(anchor: HTMLAnchorElement): boolean {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
const text = String(anchor.textContent ?? '')
|
|
|
|
|
|
.trim()
|
|
|
|
|
|
.replace(/\s+/g, '')
|
2026-04-24 23:37:16 +08:00
|
|
|
|
if (/^(link|source|citation|reference|open|原文|链接|来源|引用)$/i.test(text)) {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
return true
|
2026-04-24 23:37:16 +08:00
|
|
|
|
}
|
2026-04-23 22:54:56 +08:00
|
|
|
|
if (/[A-Za-z0-9\u3400-\u9fff]/.test(text)) {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
return false
|
2026-04-23 22:54:56 +08:00
|
|
|
|
}
|
2026-05-01 20:39:09 +08:00
|
|
|
|
if (anchor.querySelector('img, svg')) {
|
|
|
|
|
|
return true
|
2026-04-23 22:54:56 +08:00
|
|
|
|
}
|
2026-05-01 20:39:09 +08:00
|
|
|
|
return (
|
|
|
|
|
|
text.length === 0 ||
|
|
|
|
|
|
/^[\[\](){}<>#.,:;!?+\-_/\\|~`'"“”‘’·•::,。!?;、🔗↗↘↙↖↪⤴]+$/u.test(text)
|
|
|
|
|
|
)
|
2026-04-23 22:54:56 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function collectDeepSeekCitationReferenceLabels(
|
|
|
|
|
|
platform: MonitoringQuestionDetailPlatform | null,
|
|
|
|
|
|
): string[][] {
|
|
|
|
|
|
if (!isDeepSeekInlineCitationPlatform(platform?.ai_platform_id)) {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
return []
|
2026-04-23 22:54:56 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-01 20:39:09 +08:00
|
|
|
|
const rawAnswer = String(platform?.answer_text ?? '').trim()
|
|
|
|
|
|
const root = rawAnswer ? parseHtmlFragment(rawAnswer) : null
|
2026-04-23 22:54:56 +08:00
|
|
|
|
if (!root) {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
return (platform?.citations ?? []).map(() => [])
|
2026-04-23 22:54:56 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-01 20:39:09 +08:00
|
|
|
|
const citationSourceLookup = buildCitationSourceLookup(platform)
|
|
|
|
|
|
const labelsByUrl = new Map<string, string[]>()
|
|
|
|
|
|
const fallbackLabels = new Map<string, string>()
|
|
|
|
|
|
const fallbackState = { nextIndex: 1 }
|
2026-04-23 22:54:56 +08:00
|
|
|
|
|
2026-05-01 20:39:09 +08:00
|
|
|
|
for (const anchor of Array.from(root.querySelectorAll<HTMLAnchorElement>('a[href]'))) {
|
|
|
|
|
|
const normalizedURL = normalizeInlineCitationUrl(
|
|
|
|
|
|
anchor.getAttribute('href') || anchor.href || '',
|
|
|
|
|
|
)
|
2026-04-23 22:54:56 +08:00
|
|
|
|
if (!normalizedURL || !citationSourceLookup.has(normalizedURL)) {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
continue
|
2026-04-23 22:54:56 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-01 20:39:09 +08:00
|
|
|
|
const label =
|
|
|
|
|
|
normalizeDeepSeekCitationLabel(anchor.textContent) ??
|
|
|
|
|
|
getOrCreateDeepSeekFallbackCitationLabel(normalizedURL, fallbackLabels, fallbackState)
|
|
|
|
|
|
const labels = labelsByUrl.get(normalizedURL) ?? []
|
|
|
|
|
|
pushUniqueDeepSeekCitationLabel(labels, label)
|
|
|
|
|
|
labelsByUrl.set(normalizedURL, labels)
|
2026-04-23 22:54:56 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return (platform?.citations ?? []).map((citation) => {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
const normalizedURL = normalizeInlineCitationUrl(citation.cited_url)
|
|
|
|
|
|
return normalizedURL ? (labelsByUrl.get(normalizedURL) ?? []) : []
|
|
|
|
|
|
})
|
2026-04-23 22:54:56 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function resolveCitationReferenceLabels(
|
|
|
|
|
|
citationIndex: number,
|
|
|
|
|
|
platform: MonitoringQuestionDetailPlatform | null,
|
|
|
|
|
|
): string[] {
|
|
|
|
|
|
if (!isDeepSeekInlineCitationPlatform(platform?.ai_platform_id)) {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
return []
|
2026-04-23 22:54:56 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-01 20:39:09 +08:00
|
|
|
|
return activeDeepSeekCitationReferenceLabels.value[citationIndex] ?? []
|
2026-04-23 22:54:56 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function resolveCitationIndexDisplayLabels(
|
|
|
|
|
|
citationIndex: number,
|
|
|
|
|
|
platform: MonitoringQuestionDetailPlatform | null,
|
|
|
|
|
|
): string[] {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
const labels = resolveCitationReferenceLabels(citationIndex, platform)
|
2026-04-23 22:54:56 +08:00
|
|
|
|
if (!labels.length) {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
return []
|
2026-04-23 22:54:56 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-01 20:39:09 +08:00
|
|
|
|
const fallbackLabel = `[${citationIndex + 1}]`
|
2026-04-23 22:54:56 +08:00
|
|
|
|
if (labels.length === 1 && labels[0] === fallbackLabel) {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
return []
|
2026-04-23 22:54:56 +08:00
|
|
|
|
}
|
2026-05-01 20:39:09 +08:00
|
|
|
|
return labels
|
2026-04-23 22:54:56 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-24 23:37:16 +08:00
|
|
|
|
function shouldShowCitationSourceListIndex(
|
|
|
|
|
|
citationIndex: number,
|
|
|
|
|
|
platform: MonitoringQuestionDetailPlatform | null,
|
|
|
|
|
|
): boolean {
|
|
|
|
|
|
if (!isDeepSeekInlineCitationPlatform(platform?.ai_platform_id)) {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
return true
|
2026-04-24 23:37:16 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-01 20:39:09 +08:00
|
|
|
|
return resolveCitationIndexDisplayLabels(citationIndex, platform).length === 0
|
2026-04-24 23:37:16 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-23 09:11:53 +08:00
|
|
|
|
function countHanCharacters(value: string): number {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
return value.match(/[\u3400-\u9fff]/g)?.length ?? 0
|
2026-04-23 09:11:53 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function countSuspiciousMojibakeCharacters(value: string): number {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
return value.match(/[\u00c0-\u017f\u2000-\u20ff]/g)?.length ?? 0
|
2026-04-23 09:11:53 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function looksLikeNonAnswerNoise(value: string): boolean {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
const hanCount = countHanCharacters(value)
|
|
|
|
|
|
const suspiciousCount = countSuspiciousMojibakeCharacters(value)
|
2026-04-23 09:11:53 +08:00
|
|
|
|
if (hanCount === 0) {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
return suspiciousCount >= 4
|
2026-04-23 09:11:53 +08:00
|
|
|
|
}
|
2026-05-01 20:39:09 +08:00
|
|
|
|
return suspiciousCount >= 8 && suspiciousCount > hanCount * 3
|
2026-04-23 09:11:53 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function sanitizeAnswerBody(answerText: string | null | undefined): string | null {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
const normalized = String(answerText ?? '').trim()
|
2026-04-22 00:24:21 +08:00
|
|
|
|
if (!normalized) {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
return null
|
2026-04-22 00:24:21 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-23 22:54:56 +08:00
|
|
|
|
if (looksLikeHtmlAnswer(normalized)) {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
return normalized
|
2026-04-23 22:54:56 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-01 20:39:09 +08:00
|
|
|
|
const lines = normalized
|
|
|
|
|
|
.split(/\r?\n+/)
|
|
|
|
|
|
.map((line) => line.trim())
|
|
|
|
|
|
.filter(Boolean)
|
|
|
|
|
|
const keptLines = lines.filter((line) => !looksLikeNonAnswerNoise(line))
|
|
|
|
|
|
const sanitized = (keptLines.length ? keptLines.join('\n') : normalized)
|
|
|
|
|
|
.replace(/[ \t]+([,.;:!?,。!?;:])/g, '$1')
|
|
|
|
|
|
.replace(/[ \t]{2,}/g, ' ')
|
|
|
|
|
|
.replace(/\n{3,}/g, '\n\n')
|
|
|
|
|
|
.trim()
|
2026-04-22 00:24:21 +08:00
|
|
|
|
|
2026-05-01 20:39:09 +08:00
|
|
|
|
return sanitized || null
|
2026-04-23 09:11:53 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-26 10:18:35 +08:00
|
|
|
|
function normalizeLooseMarkdownLine(line: string): string {
|
|
|
|
|
|
const headingNormalizedLine = line
|
|
|
|
|
|
.replace(/^[\u200b-\u200f\ufeff]+/, '')
|
|
|
|
|
|
.replace(/^([ \t]{0,12})(?:\\#|#|#|#)/i, '$1#')
|
|
|
|
|
|
const answerScopedHeading = headingNormalizedLine.replace(
|
|
|
|
|
|
/^([ \t]{0,12})#{1,2}(?!#)\s*(?=\S)/,
|
|
|
|
|
|
'$1### ',
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
return answerScopedHeading
|
|
|
|
|
|
.replace(/^([ \t]{0,12})(#{1,6})(?=\S)/, '$1$2 ')
|
|
|
|
|
|
.replace(/^([ \t]{0,12}#{1,6}\s+)#{1,6}\s*/, '$1')
|
|
|
|
|
|
.replace(/^([ \t]{0,12}#{1,6}\s+\d+[.)、])(?=\S)/, '$1 ')
|
|
|
|
|
|
.replace(/^(\s*)([-*+])(?=\S)/, '$1$2 ')
|
|
|
|
|
|
.replace(/^(\s*)(\d{1,3})([.)])(?=\S)/, '$1$2$3 ')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function countMarkdownTablePipes(line: string): number {
|
|
|
|
|
|
return (line.match(/\|/g) ?? []).length
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function splitMarkdownTableCells(line: string): string[] {
|
|
|
|
|
|
return line
|
|
|
|
|
|
.trim()
|
|
|
|
|
|
.replace(/^\|/, '')
|
|
|
|
|
|
.replace(/\|$/, '')
|
|
|
|
|
|
.split('|')
|
|
|
|
|
|
.map((cell) => cell.trim())
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function isMarkdownTableSeparatorLine(line: string): boolean {
|
|
|
|
|
|
const normalized = line.trim()
|
|
|
|
|
|
if (!normalized.includes('|')) {
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const cells = splitMarkdownTableCells(normalized)
|
|
|
|
|
|
return cells.length >= 2 && cells.every((cell) => /^:?-{3,}:?$/.test(cell))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function countMarkdownTableCells(line: string): number {
|
|
|
|
|
|
return splitMarkdownTableCells(line).length
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function normalizeMarkdownTableLine(line: string, targetCellCount?: number): string {
|
|
|
|
|
|
let normalized = line.trim()
|
|
|
|
|
|
if (!normalized.startsWith('|')) {
|
|
|
|
|
|
normalized = `| ${normalized}`
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!normalized.endsWith('|')) {
|
|
|
|
|
|
normalized = `${normalized} |`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (targetCellCount && targetCellCount > 0) {
|
|
|
|
|
|
const cells = splitMarkdownTableCells(normalized)
|
|
|
|
|
|
if (isMarkdownTableSeparatorLine(normalized)) {
|
|
|
|
|
|
while (cells.length < targetCellCount) {
|
|
|
|
|
|
cells.push('--------')
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
while (cells.length < targetCellCount) {
|
|
|
|
|
|
cells.push('')
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (cells.length > targetCellCount) {
|
|
|
|
|
|
cells.length = targetCellCount
|
|
|
|
|
|
}
|
|
|
|
|
|
normalized = `| ${cells.join(' | ')} |`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return normalized.replace(/\s*\|\s*/g, ' | ').replace(/\s{2,}/g, ' ').trim()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function normalizeLooseMarkdownTables(content: string): string {
|
|
|
|
|
|
const detachedTableBlocks = content
|
|
|
|
|
|
.replace(/\|\|(?=\s*:?-{3,}:?\s*\|)/g, '|\n|')
|
|
|
|
|
|
.replace(
|
|
|
|
|
|
/(^|\n)(\s{0,3}#{1,6}\s+[^|\n]{1,80})\|(?=[^|\n]+\|[^\n]*\n\s*\|?\s*:?-{3,}:?\s*\|)/g,
|
|
|
|
|
|
'$1$2\n|',
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
const lines = detachedTableBlocks.split('\n').flatMap((line: string) => {
|
|
|
|
|
|
const separatorWithFirstRow = line
|
|
|
|
|
|
.trim()
|
|
|
|
|
|
.match(/^((?:\|\s*:?-{3,}:?\s*){2,}\|?)(\S.*)$/)
|
|
|
|
|
|
if (
|
|
|
|
|
|
!separatorWithFirstRow?.[1] ||
|
|
|
|
|
|
!separatorWithFirstRow[2] ||
|
|
|
|
|
|
/^\|*$/.test(separatorWithFirstRow[2].trim())
|
|
|
|
|
|
) {
|
|
|
|
|
|
return [line]
|
|
|
|
|
|
}
|
|
|
|
|
|
return [separatorWithFirstRow[1], separatorWithFirstRow[2]]
|
|
|
|
|
|
})
|
|
|
|
|
|
const normalizedLines: string[] = []
|
|
|
|
|
|
let inTable = false
|
|
|
|
|
|
let tableCellCount = 0
|
|
|
|
|
|
|
|
|
|
|
|
for (const line of lines) {
|
|
|
|
|
|
const previousIndex = normalizedLines.length - 1
|
|
|
|
|
|
if (isMarkdownTableSeparatorLine(line)) {
|
|
|
|
|
|
tableCellCount =
|
|
|
|
|
|
previousIndex >= 0
|
|
|
|
|
|
? Math.max(
|
|
|
|
|
|
countMarkdownTableCells(normalizedLines[previousIndex] ?? ''),
|
|
|
|
|
|
countMarkdownTableCells(line),
|
|
|
|
|
|
)
|
|
|
|
|
|
: countMarkdownTableCells(line)
|
|
|
|
|
|
if (
|
|
|
|
|
|
previousIndex >= 0 &&
|
|
|
|
|
|
countMarkdownTablePipes(normalizedLines[previousIndex] ?? '') >= 2
|
|
|
|
|
|
) {
|
|
|
|
|
|
normalizedLines[previousIndex] = normalizeMarkdownTableLine(
|
|
|
|
|
|
normalizedLines[previousIndex] ?? '',
|
|
|
|
|
|
tableCellCount,
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
normalizedLines.push(normalizeMarkdownTableLine(line, tableCellCount))
|
|
|
|
|
|
inTable = true
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (inTable && countMarkdownTablePipes(line) >= 2) {
|
|
|
|
|
|
normalizedLines.push(normalizeMarkdownTableLine(line, tableCellCount))
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
inTable = false
|
|
|
|
|
|
tableCellCount = 0
|
|
|
|
|
|
normalizedLines.push(line)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return normalizedLines.join('\n')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function normalizeAnswerMarkdown(content: string): string {
|
|
|
|
|
|
const headingMarkerNormalized = content
|
|
|
|
|
|
.replace(/(^|\n)[\u200b-\u200f\ufeff]*([ \t]{0,12})(?:\\#|#|#|#)/gi, '$1$2#')
|
|
|
|
|
|
.replace(/([^\n])(?=[\u200b-\u200f\ufeff]*[ \t]{0,12}(?:\\#|#|#|#|#){1,6}\s*(?:\d{1,3}[.)、]|[一二三四五六七八九十]+[、..]|第[一二三四五六七八九十0-9]+))/gi, '$1\n\n')
|
|
|
|
|
|
|
|
|
|
|
|
const detachedHeadings = normalizeLooseMarkdownTables(headingMarkerNormalized)
|
|
|
|
|
|
.replace(
|
|
|
|
|
|
/([^\n])(?=[\u200b-\u200f\ufeff]*[ \t]{0,12}#{1,6}\s*(?:\d{1,3}[.)、]|[一二三四五六七八九十]+[、..]|第[一二三四五六七八九十0-9]+))/g,
|
|
|
|
|
|
'$1\n\n',
|
|
|
|
|
|
)
|
|
|
|
|
|
.replace(/([^\n])(?=#{2,6}\s*\S)/g, '$1\n\n')
|
|
|
|
|
|
const lines = detachedHeadings.split('\n')
|
|
|
|
|
|
let inFence = false
|
|
|
|
|
|
|
|
|
|
|
|
const normalizedLines = lines.map((line) => {
|
|
|
|
|
|
if (/^\s*(?:```|~~~)/.test(line)) {
|
|
|
|
|
|
inFence = !inFence
|
|
|
|
|
|
return line
|
|
|
|
|
|
}
|
|
|
|
|
|
if (inFence) {
|
|
|
|
|
|
return line
|
|
|
|
|
|
}
|
|
|
|
|
|
return normalizeLooseMarkdownLine(line)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
return normalizedLines
|
|
|
|
|
|
.join('\n')
|
|
|
|
|
|
.replace(/\n{3,}/g, '\n\n')
|
|
|
|
|
|
.trim()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-23 09:11:53 +08:00
|
|
|
|
function buildInlineCitationMarker(
|
|
|
|
|
|
sourceGroupIndex: number,
|
|
|
|
|
|
platform: MonitoringQuestionDetailPlatform | null,
|
|
|
|
|
|
): string {
|
|
|
|
|
|
if (!Number.isFinite(sourceGroupIndex) || sourceGroupIndex < 1) {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
return ''
|
2026-04-23 09:11:53 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-01 20:39:09 +08:00
|
|
|
|
const hasCitation = Boolean(platform?.citations?.[sourceGroupIndex - 1])
|
2026-04-23 09:11:53 +08:00
|
|
|
|
const className = hasCitation
|
2026-05-01 20:39:09 +08:00
|
|
|
|
? 'tracking-inline-citation'
|
|
|
|
|
|
: 'tracking-inline-citation tracking-inline-citation--muted'
|
2026-04-23 09:11:53 +08:00
|
|
|
|
|
|
|
|
|
|
if (!hasCitation) {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
return `<span class="${className}">[${sourceGroupIndex}]</span>`
|
2026-04-23 09:11:53 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-01 20:39:09 +08:00
|
|
|
|
return `<button type="button" class="${className}" data-citation-index="${sourceGroupIndex}">[${sourceGroupIndex}]</button>`
|
2026-04-23 09:11:53 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-23 22:54:56 +08:00
|
|
|
|
function renderDeepSeekAnswerContent(
|
|
|
|
|
|
answerText: string,
|
|
|
|
|
|
platform: MonitoringQuestionDetailPlatform | null,
|
|
|
|
|
|
): string {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
const root = parseHtmlFragment(answerText)
|
2026-04-23 22:54:56 +08:00
|
|
|
|
if (!root) {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
return answerText
|
2026-04-23 22:54:56 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-01 20:39:09 +08:00
|
|
|
|
const inlineCitationLookup = buildInlineCitationSourceLookup(platform)
|
|
|
|
|
|
const citationSourceLookup = buildCitationSourceLookup(platform)
|
|
|
|
|
|
const fallbackLabels = new Map<string, string>()
|
|
|
|
|
|
const fallbackState = { nextIndex: 1 }
|
2026-04-23 22:54:56 +08:00
|
|
|
|
|
2026-05-01 20:39:09 +08:00
|
|
|
|
for (const anchor of Array.from(root.querySelectorAll<HTMLAnchorElement>('a[href], a'))) {
|
|
|
|
|
|
const rawHref = anchor.hasAttribute('href')
|
|
|
|
|
|
? anchor.getAttribute('href') || anchor.href || ''
|
|
|
|
|
|
: ''
|
|
|
|
|
|
const normalizedURL = normalizeInlineCitationUrl(rawHref)
|
2026-04-23 22:54:56 +08:00
|
|
|
|
if (!normalizedURL && !shouldStripDeepSeekOrphanAnchor(anchor)) {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
continue
|
2026-04-23 22:54:56 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-01 20:39:09 +08:00
|
|
|
|
const source = inlineCitationLookup.get(normalizedURL) ?? null
|
|
|
|
|
|
const citationSource = citationSourceLookup.get(normalizedURL) ?? null
|
2026-04-23 22:54:56 +08:00
|
|
|
|
if (!source && !citationSource) {
|
|
|
|
|
|
if (shouldStripDeepSeekOrphanAnchor(anchor)) {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
anchor.remove()
|
2026-04-24 23:37:16 +08:00
|
|
|
|
} else {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
anchor.replaceWith(document.createTextNode(anchor.textContent ?? ''))
|
2026-04-23 22:54:56 +08:00
|
|
|
|
}
|
2026-05-01 20:39:09 +08:00
|
|
|
|
continue
|
2026-04-23 22:54:56 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-01 20:39:09 +08:00
|
|
|
|
anchor.setAttribute('href', normalizedURL)
|
|
|
|
|
|
anchor.setAttribute('target', '_blank')
|
|
|
|
|
|
anchor.setAttribute('rel', 'noreferrer')
|
|
|
|
|
|
anchor.setAttribute('class', 'tracking-inline-citation tracking-inline-citation--content')
|
2026-04-23 22:54:56 +08:00
|
|
|
|
if (citationSource) {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
anchor.setAttribute('data-citation-index', String(citationSource.sourceGroupIndex))
|
2026-04-23 22:54:56 +08:00
|
|
|
|
} else {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
anchor.removeAttribute('data-citation-index')
|
2026-04-23 22:54:56 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const title = citationSource
|
|
|
|
|
|
? resolveCitationTitle(citationSource.citation)
|
2026-05-01 20:39:09 +08:00
|
|
|
|
: source?.title?.trim() || ''
|
2026-04-23 22:54:56 +08:00
|
|
|
|
if (title) {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
anchor.setAttribute('title', title)
|
2026-04-23 22:54:56 +08:00
|
|
|
|
} else {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
anchor.removeAttribute('title')
|
2026-04-23 22:54:56 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-01 20:39:09 +08:00
|
|
|
|
const displayLabel =
|
|
|
|
|
|
normalizeDeepSeekCitationLabel(anchor.textContent) ??
|
|
|
|
|
|
getOrCreateDeepSeekFallbackCitationLabel(normalizedURL, fallbackLabels, fallbackState)
|
|
|
|
|
|
anchor.textContent = displayLabel
|
2026-04-23 22:54:56 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-01 20:39:09 +08:00
|
|
|
|
return root.innerHTML.trim() || answerText
|
2026-04-23 22:54:56 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-23 09:11:53 +08:00
|
|
|
|
function formatAnswerContent(platform: MonitoringQuestionDetailPlatform | null): string | null {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
const rawAnswer = String(platform?.answer_text ?? '').trim()
|
2026-04-23 22:54:56 +08:00
|
|
|
|
if (!rawAnswer) {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
return null
|
2026-04-23 22:54:56 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-01 20:39:09 +08:00
|
|
|
|
if (
|
|
|
|
|
|
isDeepSeekInlineCitationPlatform(platform?.ai_platform_id) &&
|
|
|
|
|
|
looksLikeHtmlAnswer(rawAnswer)
|
|
|
|
|
|
) {
|
|
|
|
|
|
return renderDeepSeekAnswerContent(rawAnswer, platform)
|
2026-04-23 22:54:56 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (looksLikeHtmlAnswer(rawAnswer)) {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
return rawAnswer
|
2026-04-23 22:54:56 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-01 20:39:09 +08:00
|
|
|
|
const sanitized = sanitizeAnswerBody(rawAnswer)
|
2026-04-23 09:11:53 +08:00
|
|
|
|
if (!sanitized) {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
return null
|
2026-04-23 09:11:53 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const withInlineMarkers = isLinkedInlineCitationPlatform(platform?.ai_platform_id)
|
|
|
|
|
|
? sanitized
|
2026-05-01 20:39:09 +08:00
|
|
|
|
.replace(createQwenSourceGroupPattern(), (_, sourceGroupIndex) => {
|
|
|
|
|
|
return buildInlineCitationMarker(Number(sourceGroupIndex), platform)
|
|
|
|
|
|
})
|
|
|
|
|
|
.replace(createYuanbaoCitationPattern(), (_, sourceGroupIndex) => {
|
|
|
|
|
|
return buildInlineCitationMarker(Number(sourceGroupIndex), platform)
|
|
|
|
|
|
})
|
|
|
|
|
|
: sanitized
|
2026-04-23 09:11:53 +08:00
|
|
|
|
|
2026-05-26 10:18:35 +08:00
|
|
|
|
const normalizedMarkdown = withInlineMarkers
|
2026-05-01 20:39:09 +08:00
|
|
|
|
.replace(createQwenSourceGroupStripPattern(), '')
|
|
|
|
|
|
.replace(createYuanbaoCitationStripPattern(), '')
|
|
|
|
|
|
.replace(/[ \t]{2,}/g, ' ')
|
|
|
|
|
|
.replace(/\n{3,}/g, '\n\n')
|
|
|
|
|
|
.trim()
|
2026-05-26 10:18:35 +08:00
|
|
|
|
|
|
|
|
|
|
return normalizeAnswerMarkdown(normalizedMarkdown)
|
2026-04-22 00:24:21 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-23 22:54:56 +08:00
|
|
|
|
function collectDeepSeekInlineCitationOccurrences(
|
|
|
|
|
|
answerText: string,
|
|
|
|
|
|
platform: MonitoringQuestionDetailPlatform | null,
|
|
|
|
|
|
): Array<{
|
2026-05-01 20:39:09 +08:00
|
|
|
|
url: string
|
|
|
|
|
|
label: string | null
|
|
|
|
|
|
siteName: string | null
|
|
|
|
|
|
citationCount: number
|
2026-04-23 22:54:56 +08:00
|
|
|
|
}> {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
const root = parseHtmlFragment(answerText)
|
2026-04-23 22:54:56 +08:00
|
|
|
|
if (!root) {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
return []
|
2026-04-23 22:54:56 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-01 20:39:09 +08:00
|
|
|
|
const sourceLookup = buildDeepSeekAnswerLinkSourceLookup(platform)
|
2026-04-23 22:54:56 +08:00
|
|
|
|
|
2026-05-01 20:39:09 +08:00
|
|
|
|
const counts = new Map<
|
|
|
|
|
|
string,
|
|
|
|
|
|
{ label: string | null; siteName: string | null; citationCount: number }
|
|
|
|
|
|
>()
|
|
|
|
|
|
for (const anchor of Array.from(root.querySelectorAll<HTMLAnchorElement>('a[href]'))) {
|
|
|
|
|
|
const normalizedURL = normalizeInlineCitationUrl(
|
|
|
|
|
|
anchor.getAttribute('href') || anchor.href || '',
|
|
|
|
|
|
)
|
2026-04-24 23:37:16 +08:00
|
|
|
|
if (!normalizedURL || !shouldCountDeepSeekAnswerLink(anchor)) {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
continue
|
2026-04-23 22:54:56 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-01 20:39:09 +08:00
|
|
|
|
const source = sourceLookup.get(normalizedURL) ?? null
|
|
|
|
|
|
const existing = counts.get(normalizedURL)
|
2026-04-23 22:54:56 +08:00
|
|
|
|
if (existing) {
|
2026-04-24 23:37:16 +08:00
|
|
|
|
// One row per URL, but every visible answer occurrence contributes to the count.
|
2026-05-01 20:39:09 +08:00
|
|
|
|
existing.citationCount += 1
|
|
|
|
|
|
continue
|
2026-04-23 22:54:56 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
counts.set(normalizedURL, {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
label: source?.title || anchor.getAttribute('title')?.trim() || null,
|
2026-04-24 23:37:16 +08:00
|
|
|
|
siteName: source?.siteName || null,
|
2026-04-23 22:54:56 +08:00
|
|
|
|
citationCount: 1,
|
2026-05-01 20:39:09 +08:00
|
|
|
|
})
|
2026-04-23 22:54:56 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return Array.from(counts.entries()).map(([url, value]) => ({
|
|
|
|
|
|
url,
|
|
|
|
|
|
label: value.label,
|
|
|
|
|
|
siteName: value.siteName,
|
|
|
|
|
|
citationCount: value.citationCount,
|
2026-05-01 20:39:09 +08:00
|
|
|
|
}))
|
2026-04-23 22:54:56 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-22 00:24:21 +08:00
|
|
|
|
function analyzeInlineContentCitations(
|
|
|
|
|
|
platform: MonitoringQuestionDetailPlatform | null,
|
|
|
|
|
|
): TrackingAnalyzedContentCitation[] {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
const answerText = String(platform?.answer_text ?? '')
|
2026-04-22 00:24:21 +08:00
|
|
|
|
if (!answerText) {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
return []
|
2026-04-22 00:24:21 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-23 22:54:56 +08:00
|
|
|
|
if (isDeepSeekInlineCitationPlatform(platform?.ai_platform_id)) {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
const occurrences = collectDeepSeekInlineCitationOccurrences(answerText, platform)
|
2026-04-23 22:54:56 +08:00
|
|
|
|
if (!occurrences.length) {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
return []
|
2026-04-23 22:54:56 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-01 20:39:09 +08:00
|
|
|
|
const totalCitationCount = occurrences.reduce((sum, item) => sum + item.citationCount, 0)
|
2026-04-23 22:54:56 +08:00
|
|
|
|
if (!totalCitationCount) {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
return []
|
2026-04-23 22:54:56 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return occurrences
|
|
|
|
|
|
.sort((left, right) => {
|
|
|
|
|
|
if (right.citationCount !== left.citationCount) {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
return right.citationCount - left.citationCount
|
2026-04-23 22:54:56 +08:00
|
|
|
|
}
|
2026-05-01 20:39:09 +08:00
|
|
|
|
return left.url.localeCompare(right.url, 'zh-CN')
|
2026-04-23 22:54:56 +08:00
|
|
|
|
})
|
|
|
|
|
|
.map((item) => ({
|
|
|
|
|
|
key: `deepseek-${item.url}`,
|
|
|
|
|
|
contentName: item.label || item.url,
|
|
|
|
|
|
channelName: item.siteName || deriveCitationChannelName(item.url),
|
|
|
|
|
|
citationCount: item.citationCount,
|
|
|
|
|
|
citationRate: item.citationCount / totalCitationCount,
|
|
|
|
|
|
citedURL: item.url,
|
2026-05-01 20:39:09 +08:00
|
|
|
|
}))
|
2026-04-23 22:54:56 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-23 09:11:53 +08:00
|
|
|
|
if (isKimiInlineCitationPlatform(platform?.ai_platform_id)) {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
const occurrences = collectKimiInlineCitationOccurrences(answerText)
|
2026-04-23 09:11:53 +08:00
|
|
|
|
if (!occurrences.length) {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
return []
|
2026-04-23 09:11:53 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-01 20:39:09 +08:00
|
|
|
|
const totalCitationCount = occurrences.reduce((sum, item) => sum + item.citationCount, 0)
|
2026-04-23 09:11:53 +08:00
|
|
|
|
if (!totalCitationCount) {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
return []
|
2026-04-23 09:11:53 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-01 20:39:09 +08:00
|
|
|
|
const inlineCitationLookup = buildInlineCitationSourceLookup(platform)
|
2026-04-23 09:11:53 +08:00
|
|
|
|
return occurrences
|
|
|
|
|
|
.sort((left, right) => {
|
|
|
|
|
|
if (right.citationCount !== left.citationCount) {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
return right.citationCount - left.citationCount
|
2026-04-23 09:11:53 +08:00
|
|
|
|
}
|
2026-05-01 20:39:09 +08:00
|
|
|
|
return left.url.localeCompare(right.url, 'zh-CN')
|
2026-04-23 09:11:53 +08:00
|
|
|
|
})
|
|
|
|
|
|
.map((item) => {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
const source = inlineCitationLookup.get(item.url)
|
|
|
|
|
|
const contentName = source?.title?.trim() || item.label || item.url
|
|
|
|
|
|
const channelName = source?.site_name?.trim() || deriveCitationChannelName(item.url)
|
2026-04-23 09:11:53 +08:00
|
|
|
|
return {
|
|
|
|
|
|
key: `kimi-${item.url}`,
|
|
|
|
|
|
contentName,
|
|
|
|
|
|
channelName,
|
|
|
|
|
|
citationCount: item.citationCount,
|
|
|
|
|
|
citationRate: item.citationCount / totalCitationCount,
|
|
|
|
|
|
citedURL: item.url,
|
2026-05-01 20:39:09 +08:00
|
|
|
|
}
|
|
|
|
|
|
})
|
2026-04-23 09:11:53 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-25 16:48:28 +08:00
|
|
|
|
if (!isLinkedInlineCitationPlatform(platform?.ai_platform_id)) {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
return []
|
2026-04-25 16:48:28 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-01 20:39:09 +08:00
|
|
|
|
const sourceGroupIndexes = collectInlineCitationIndexes(answerText)
|
2026-04-23 09:11:53 +08:00
|
|
|
|
if (!sourceGroupIndexes.length) {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
return []
|
2026-04-22 00:24:21 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-01 20:39:09 +08:00
|
|
|
|
const citationCounts = new Map<number, number>()
|
2026-04-23 09:11:53 +08:00
|
|
|
|
for (const sourceGroupIndex of sourceGroupIndexes) {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
citationCounts.set(sourceGroupIndex, (citationCounts.get(sourceGroupIndex) ?? 0) + 1)
|
2026-04-22 00:24:21 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-01 20:39:09 +08:00
|
|
|
|
const totalCitationCount = Array.from(citationCounts.values()).reduce(
|
|
|
|
|
|
(sum, count) => sum + count,
|
|
|
|
|
|
0,
|
|
|
|
|
|
)
|
2026-04-22 00:24:21 +08:00
|
|
|
|
if (!totalCitationCount) {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
return []
|
2026-04-22 00:24:21 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return Array.from(citationCounts.entries())
|
|
|
|
|
|
.sort((left, right) => {
|
|
|
|
|
|
if (right[1] !== left[1]) {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
return right[1] - left[1]
|
2026-04-22 00:24:21 +08:00
|
|
|
|
}
|
2026-05-01 20:39:09 +08:00
|
|
|
|
return left[0] - right[0]
|
2026-04-22 00:24:21 +08:00
|
|
|
|
})
|
2026-04-23 09:11:53 +08:00
|
|
|
|
.flatMap(([sourceGroupIndex, citationCount]) => {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
const citation = platform?.citations?.[sourceGroupIndex - 1] ?? null
|
|
|
|
|
|
const citedURL = citation?.cited_url?.trim() ?? ''
|
2026-04-23 09:11:53 +08:00
|
|
|
|
if (!citedURL) {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
return []
|
2026-04-23 09:11:53 +08:00
|
|
|
|
}
|
2026-04-22 00:24:21 +08:00
|
|
|
|
|
2026-05-01 20:39:09 +08:00
|
|
|
|
return [
|
|
|
|
|
|
{
|
|
|
|
|
|
key: `${sourceGroupIndex}-${citedURL}`,
|
|
|
|
|
|
contentName: citation ? resolveCitationTitle(citation) : `引用内容 #${sourceGroupIndex}`,
|
|
|
|
|
|
channelName: citation?.site_name ?? '--',
|
|
|
|
|
|
citationCount,
|
|
|
|
|
|
citationRate: citationCount / totalCitationCount,
|
|
|
|
|
|
citedURL,
|
|
|
|
|
|
},
|
|
|
|
|
|
]
|
|
|
|
|
|
})
|
2026-04-22 00:24:21 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-23 09:11:53 +08:00
|
|
|
|
function clearCitationHighlight(): void {
|
|
|
|
|
|
if (citationHighlightTimer !== null) {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
clearTimeout(citationHighlightTimer)
|
|
|
|
|
|
citationHighlightTimer = null
|
2026-04-23 09:11:53 +08:00
|
|
|
|
}
|
2026-05-01 20:39:09 +08:00
|
|
|
|
highlightedCitationIndex.value = null
|
2026-04-23 09:11:53 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function focusCitationSource(sourceGroupIndex: number): Promise<void> {
|
|
|
|
|
|
if (!Number.isFinite(sourceGroupIndex) || sourceGroupIndex < 1) {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
return
|
2026-04-23 09:11:53 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-01 20:39:09 +08:00
|
|
|
|
await nextTick()
|
2026-04-23 09:11:53 +08:00
|
|
|
|
const citationCard = citationSourcesScrollRef.value?.querySelector<HTMLElement>(
|
|
|
|
|
|
`[data-citation-index="${sourceGroupIndex}"]`,
|
2026-05-01 20:39:09 +08:00
|
|
|
|
)
|
2026-04-23 09:11:53 +08:00
|
|
|
|
if (!citationCard) {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
return
|
2026-04-23 09:11:53 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-01 20:39:09 +08:00
|
|
|
|
highlightedCitationIndex.value = sourceGroupIndex
|
2026-04-23 09:11:53 +08:00
|
|
|
|
citationCard.scrollIntoView({
|
2026-05-01 20:39:09 +08:00
|
|
|
|
behavior: 'smooth',
|
|
|
|
|
|
block: 'nearest',
|
|
|
|
|
|
inline: 'nearest',
|
|
|
|
|
|
})
|
2026-04-23 09:11:53 +08:00
|
|
|
|
|
|
|
|
|
|
if (citationHighlightTimer !== null) {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
clearTimeout(citationHighlightTimer)
|
2026-04-23 09:11:53 +08:00
|
|
|
|
}
|
|
|
|
|
|
citationHighlightTimer = setTimeout(() => {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
highlightedCitationIndex.value = null
|
|
|
|
|
|
citationHighlightTimer = null
|
|
|
|
|
|
}, 2400)
|
2026-04-23 09:11:53 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function handleAnswerContentClick(event: MouseEvent): void {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
const target = event.target
|
2026-04-23 09:11:53 +08:00
|
|
|
|
if (!(target instanceof HTMLElement)) {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
return
|
2026-04-23 09:11:53 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-01 20:39:09 +08:00
|
|
|
|
const trigger = target.closest('[data-citation-index]') as HTMLElement | null
|
2026-04-23 09:11:53 +08:00
|
|
|
|
if (!trigger) {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
return
|
2026-04-23 09:11:53 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-01 20:39:09 +08:00
|
|
|
|
const sourceGroupIndex = Number(trigger.dataset.citationIndex ?? '')
|
2026-04-23 09:11:53 +08:00
|
|
|
|
if (!Number.isFinite(sourceGroupIndex) || sourceGroupIndex < 1) {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
return
|
2026-04-23 09:11:53 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-01 20:39:09 +08:00
|
|
|
|
event.preventDefault()
|
|
|
|
|
|
void focusCitationSource(sourceGroupIndex)
|
2026-04-23 09:11:53 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-12 09:56:18 +08:00
|
|
|
|
function normalizeQueryValue(value: unknown): string {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
const raw = Array.isArray(value) ? value[0] : value
|
|
|
|
|
|
return String(raw ?? '').trim()
|
2026-04-12 09:56:18 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-24 00:27:32 +08:00
|
|
|
|
function normalizeHistoryStateValue(value: unknown): string {
|
|
|
|
|
|
return typeof value === 'string' ? value.trim() : ''
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-12 09:56:18 +08:00
|
|
|
|
function normalizeTrackingBusinessDate(value: unknown): string | null {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
const normalized = normalizeQueryValue(value)
|
2026-04-12 09:56:18 +08:00
|
|
|
|
if (!normalized) {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
return null
|
2026-04-12 09:56:18 +08:00
|
|
|
|
}
|
2026-05-01 20:39:09 +08:00
|
|
|
|
const parsed = dayjs(normalized)
|
|
|
|
|
|
if (!parsed.isValid() || parsed.format('YYYY-MM-DD') !== normalized) {
|
|
|
|
|
|
return null
|
2026-04-12 09:56:18 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-01 20:39:09 +08:00
|
|
|
|
const today = dayjs().startOf('day')
|
|
|
|
|
|
const earliestAllowed = today.subtract(trackingMaxHistoryDays - 1, 'day')
|
|
|
|
|
|
if (parsed.isAfter(today, 'day') || parsed.isBefore(earliestAllowed, 'day')) {
|
|
|
|
|
|
return null
|
2026-04-12 09:56:18 +08:00
|
|
|
|
}
|
2026-05-01 20:39:09 +08:00
|
|
|
|
return normalized
|
2026-04-12 09:56:18 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function parsePositiveNumber(value: unknown): number | null {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
const raw = Array.isArray(value) ? value[0] : value
|
|
|
|
|
|
const parsed = Number(raw)
|
2026-04-12 09:56:18 +08:00
|
|
|
|
if (!Number.isFinite(parsed) || parsed <= 0) {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
return null
|
2026-04-12 09:56:18 +08:00
|
|
|
|
}
|
2026-05-01 20:39:09 +08:00
|
|
|
|
return parsed
|
2026-04-12 09:56:18 +08:00
|
|
|
|
}
|
2026-04-23 09:11:53 +08:00
|
|
|
|
|
|
|
|
|
|
const citationAnalysisColumns = computed(() => [
|
2026-05-01 20:39:09 +08:00
|
|
|
|
{
|
|
|
|
|
|
title: t('tracking.columns.sourcePlatform'),
|
|
|
|
|
|
key: 'sourcePlatform',
|
|
|
|
|
|
dataIndex: 'site_name',
|
|
|
|
|
|
width: '30%',
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: t('tracking.columns.domain'),
|
|
|
|
|
|
key: 'domain',
|
|
|
|
|
|
dataIndex: 'site_domain',
|
|
|
|
|
|
width: '30%',
|
|
|
|
|
|
align: 'center' as const,
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: t('tracking.columns.citations'),
|
|
|
|
|
|
key: 'citationCount',
|
|
|
|
|
|
dataIndex: 'citation_count',
|
|
|
|
|
|
align: 'center' as const,
|
|
|
|
|
|
width: '20%',
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: t('tracking.columns.share'),
|
|
|
|
|
|
key: 'citationRate',
|
|
|
|
|
|
dataIndex: 'citation_rate',
|
|
|
|
|
|
align: 'center' as const,
|
|
|
|
|
|
width: '20%',
|
|
|
|
|
|
},
|
|
|
|
|
|
])
|
2026-04-23 09:11:53 +08:00
|
|
|
|
|
|
|
|
|
|
const contentCitationColumns = computed(() => [
|
2026-05-01 20:39:09 +08:00
|
|
|
|
{
|
|
|
|
|
|
title: t('tracking.columns.contentName'),
|
|
|
|
|
|
key: 'contentName',
|
|
|
|
|
|
dataIndex: 'contentName',
|
|
|
|
|
|
width: '38%',
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: t('tracking.columns.channelName'),
|
|
|
|
|
|
key: 'channelName',
|
|
|
|
|
|
dataIndex: 'channelName',
|
|
|
|
|
|
width: '20%',
|
|
|
|
|
|
align: 'center' as const,
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: t('tracking.columns.citations'),
|
|
|
|
|
|
key: 'citationCount',
|
|
|
|
|
|
dataIndex: 'citationCount',
|
|
|
|
|
|
align: 'center' as const,
|
|
|
|
|
|
width: '16%',
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: t('tracking.columns.share'),
|
|
|
|
|
|
key: 'citationRate',
|
|
|
|
|
|
dataIndex: 'citationRate',
|
|
|
|
|
|
align: 'center' as const,
|
|
|
|
|
|
width: '16%',
|
|
|
|
|
|
},
|
|
|
|
|
|
{ title: t('common.actions'), key: 'actions', align: 'center' as const, width: '10%' },
|
|
|
|
|
|
])
|
2026-04-12 09:56:18 +08:00
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<template>
|
|
|
|
|
|
<section class="tracking-question-page">
|
|
|
|
|
|
<div class="tracking-question-page__hero">
|
|
|
|
|
|
<div class="tracking-question-page__header-top">
|
|
|
|
|
|
<button type="button" class="tracking-question-page__back" @click="goBack">
|
|
|
|
|
|
<ArrowLeftOutlined />
|
2026-05-01 20:39:09 +08:00
|
|
|
|
<span>{{ t('tracking.backToQuestions') }}</span>
|
2026-04-12 09:56:18 +08:00
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="tracking-question-page__copy">
|
2026-05-01 20:39:09 +08:00
|
|
|
|
<p class="tracking-question-page__eyebrow">{{ t('tracking.questionDetailEyebrow') }}</p>
|
2026-04-12 09:56:18 +08:00
|
|
|
|
<h1>{{ pageTitle }}</h1>
|
|
|
|
|
|
<p class="tracking-question-page__meta">
|
2026-05-01 20:39:09 +08:00
|
|
|
|
{{ t('tracking.questionDetailCollectedAt', { time: collectedAtDisplay }) }}
|
2026-04-12 09:56:18 +08:00
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<a-alert
|
|
|
|
|
|
v-if="detailQuery.error.value"
|
|
|
|
|
|
type="error"
|
|
|
|
|
|
show-icon
|
|
|
|
|
|
:message="formatError(detailQuery.error.value)"
|
|
|
|
|
|
class="tracking-question-page__alert"
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
|
|
<a-spin :spinning="detailQuery.isLoading.value">
|
|
|
|
|
|
<div class="tracking-question-page__content">
|
2026-04-24 22:21:01 +08:00
|
|
|
|
<a-alert
|
|
|
|
|
|
v-if="shouldShowDetailUnavailableNotice"
|
|
|
|
|
|
:type="detailUnavailableNoticeType"
|
|
|
|
|
|
show-icon
|
|
|
|
|
|
:message="detailUnavailableNoticeTitle"
|
|
|
|
|
|
:description="detailUnavailableNoticeDescription"
|
|
|
|
|
|
class="tracking-question-page__alert"
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
2026-05-01 20:39:09 +08:00
|
|
|
|
<div v-if="!shouldShowDetailUnavailableNotice" class="tracking-question-page__tabs-wrapper">
|
|
|
|
|
|
<a-tabs v-model:activeKey="activePlatformId" size="large" class="tracking-question-tabs">
|
|
|
|
|
|
<a-tab-pane v-for="platform in detailPlatforms" :key="platform.ai_platform_id">
|
|
|
|
|
|
<template #tab>
|
|
|
|
|
|
<span :class="platform.sample_status !== 'sampled' ? 'is-muted' : ''">
|
|
|
|
|
|
{{ platform.platform_name }}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</a-tab-pane>
|
|
|
|
|
|
</a-tabs>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div v-if="!shouldShowDetailUnavailableNotice" class="tracking-question-grid">
|
|
|
|
|
|
<section class="tracking-question-card tracking-question-card--answer">
|
|
|
|
|
|
<div class="tracking-question-card__header">
|
|
|
|
|
|
<div class="tracking-question-card__section-title">
|
|
|
|
|
|
<span class="tracking-question-card__accent" />
|
|
|
|
|
|
<span>{{ t('tracking.answerContent') }}</span>
|
|
|
|
|
|
</div>
|
2026-04-12 09:56:18 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-05-01 20:39:09 +08:00
|
|
|
|
<div class="tracking-question-card__body">
|
|
|
|
|
|
<div class="tracking-question-answer__headline">
|
|
|
|
|
|
<h2>{{ activePlatform?.platform_name ?? t('common.noData') }}</h2>
|
|
|
|
|
|
<a-tag :color="statusTagColor(activePlatform?.sample_status ?? 'pending')">
|
|
|
|
|
|
{{ statusLabel(activePlatform?.sample_status ?? 'pending') }}
|
|
|
|
|
|
</a-tag>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="tracking-question-answer__content custom-scrollbar">
|
|
|
|
|
|
<MarkdownPreview
|
|
|
|
|
|
v-if="renderedAnswerContent"
|
|
|
|
|
|
:content="renderedAnswerContent"
|
|
|
|
|
|
@click="handleAnswerContentClick"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<a-empty v-else :description="t('tracking.noAnswer')" />
|
|
|
|
|
|
</div>
|
2026-04-12 09:56:18 +08:00
|
|
|
|
|
2026-05-01 20:39:09 +08:00
|
|
|
|
<div class="tracking-question-answer__meta">
|
|
|
|
|
|
<span>
|
|
|
|
|
|
{{
|
|
|
|
|
|
activePlatform?.sampled_at ? formatDateTime(activePlatform.sampled_at) : '--'
|
|
|
|
|
|
}}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<span>{{ resolveAnswerProviderModel(activePlatform) }}</span>
|
|
|
|
|
|
</div>
|
2026-04-12 09:56:18 +08:00
|
|
|
|
</div>
|
2026-05-01 20:39:09 +08:00
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
|
|
<section class="tracking-question-card">
|
|
|
|
|
|
<div class="tracking-question-card__header">
|
|
|
|
|
|
<div class="tracking-question-card__section-title">
|
|
|
|
|
|
<span class="tracking-question-card__accent" />
|
|
|
|
|
|
<span>{{ t('tracking.citationSourcesTitle') }}</span>
|
|
|
|
|
|
</div>
|
2026-04-12 09:56:18 +08:00
|
|
|
|
</div>
|
2026-05-01 20:39:09 +08:00
|
|
|
|
|
|
|
|
|
|
<div
|
|
|
|
|
|
ref="citationSourcesScrollRef"
|
|
|
|
|
|
class="tracking-question-card__scroll-area custom-scrollbar"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div v-if="activePlatform?.citations?.length" class="tracking-question-source-list">
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-for="(citation, index) in activePlatform.citations"
|
|
|
|
|
|
:key="citation.cited_url"
|
|
|
|
|
|
role="link"
|
|
|
|
|
|
tabindex="0"
|
|
|
|
|
|
class="tracking-question-source-card"
|
|
|
|
|
|
:class="{ 'is-linked': highlightedCitationIndex === index + 1 }"
|
|
|
|
|
|
:data-citation-index="index + 1"
|
|
|
|
|
|
@click="openExternalURL(citation.cited_url)"
|
|
|
|
|
|
@keydown.enter="openExternalURL(citation.cited_url)"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div class="tracking-question-source-card__headline">
|
|
|
|
|
|
<div class="tracking-question-source-card__indexes">
|
|
|
|
|
|
<span
|
|
|
|
|
|
v-for="label in resolveCitationIndexDisplayLabels(index, activePlatform)"
|
|
|
|
|
|
:key="`${citation.cited_url}-${label}`"
|
|
|
|
|
|
class="tracking-question-source-card__reference-badge"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ label }}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<span
|
|
|
|
|
|
v-if="shouldShowCitationSourceListIndex(index, activePlatform)"
|
|
|
|
|
|
class="tracking-question-source-card__index"
|
|
|
|
|
|
:class="{
|
|
|
|
|
|
'tracking-question-source-card__index--secondary':
|
|
|
|
|
|
resolveCitationIndexDisplayLabels(index, activePlatform).length > 0,
|
|
|
|
|
|
}"
|
|
|
|
|
|
>
|
|
|
|
|
|
[{{ index + 1 }}]
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<strong>{{ citation.site_name }}</strong>
|
2026-04-23 22:54:56 +08:00
|
|
|
|
<span
|
2026-05-01 20:39:09 +08:00
|
|
|
|
v-if="resolveCitationTitle(citation)"
|
|
|
|
|
|
class="tracking-question-source-card__divider"
|
2026-04-23 22:54:56 +08:00
|
|
|
|
>
|
2026-05-01 20:39:09 +08:00
|
|
|
|
·
|
2026-04-23 22:54:56 +08:00
|
|
|
|
</span>
|
|
|
|
|
|
<span
|
2026-05-01 20:39:09 +08:00
|
|
|
|
class="tracking-question-source-card__title"
|
|
|
|
|
|
:title="resolveCitationTitle(citation)"
|
2026-04-23 22:54:56 +08:00
|
|
|
|
>
|
2026-05-01 20:39:09 +08:00
|
|
|
|
{{ resolveCitationTitle(citation) }}
|
2026-04-23 22:54:56 +08:00
|
|
|
|
</span>
|
2026-05-01 20:39:09 +08:00
|
|
|
|
<a-tooltip :title="t('tracking.imitationAction')">
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
class="tracking-question-imitation-trigger tracking-question-imitation-trigger--source"
|
|
|
|
|
|
@click.stop="
|
|
|
|
|
|
openImitationCreate(citation.cited_url, resolveCitationTitle(citation))
|
|
|
|
|
|
"
|
|
|
|
|
|
>
|
|
|
|
|
|
<CopyOutlined />
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</a-tooltip>
|
|
|
|
|
|
<a-tag
|
|
|
|
|
|
v-if="
|
|
|
|
|
|
citation.article_id &&
|
|
|
|
|
|
supportsContentCitationDisplay(activePlatform?.ai_platform_id)
|
|
|
|
|
|
"
|
|
|
|
|
|
color="blue"
|
|
|
|
|
|
class="tracking-question-source-card__badge"
|
2026-04-25 21:36:44 +08:00
|
|
|
|
>
|
2026-05-01 20:39:09 +08:00
|
|
|
|
{{ t('tracking.contentCitationBadge') }}
|
|
|
|
|
|
</a-tag>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="tracking-question-source-card__link">
|
|
|
|
|
|
{{ citation.cited_url }}
|
|
|
|
|
|
</div>
|
2026-04-12 09:56:18 +08:00
|
|
|
|
</div>
|
2026-04-25 21:36:44 +08:00
|
|
|
|
</div>
|
2026-05-01 20:39:09 +08:00
|
|
|
|
<a-empty v-else :description="t('tracking.noCitations')" />
|
2026-04-12 09:56:18 +08:00
|
|
|
|
</div>
|
2026-05-01 20:39:09 +08:00
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Citation Analysis -->
|
|
|
|
|
|
<section
|
|
|
|
|
|
:class="[
|
|
|
|
|
|
'tracking-question-card',
|
|
|
|
|
|
!shouldShowInlineContentCitations ? 'tracking-question-card--span-2' : '',
|
|
|
|
|
|
]"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div class="tracking-question-card__header">
|
|
|
|
|
|
<div class="tracking-question-card__section-title">
|
|
|
|
|
|
<span class="tracking-question-card__accent" />
|
|
|
|
|
|
<span>{{ t('tracking.citationAnalysisTitle') }}</span>
|
|
|
|
|
|
</div>
|
2026-04-12 09:56:18 +08:00
|
|
|
|
</div>
|
2026-05-01 20:39:09 +08:00
|
|
|
|
|
|
|
|
|
|
<div
|
|
|
|
|
|
class="tracking-question-card__scroll-area custom-scrollbar tracking-question-table-wrap"
|
2026-04-23 09:11:53 +08:00
|
|
|
|
>
|
2026-05-01 20:39:09 +08:00
|
|
|
|
<a-table
|
|
|
|
|
|
v-if="activeCitationAnalysis.length"
|
|
|
|
|
|
row-key="site_key"
|
|
|
|
|
|
class="modern-table"
|
|
|
|
|
|
:columns="citationAnalysisColumns"
|
|
|
|
|
|
:data-source="activeCitationAnalysis"
|
|
|
|
|
|
:pagination="false"
|
|
|
|
|
|
size="middle"
|
|
|
|
|
|
>
|
|
|
|
|
|
<template #bodyCell="{ column, record }">
|
|
|
|
|
|
<template v-if="column.key === 'sourcePlatform'">
|
|
|
|
|
|
<div class="cell-primary-stack">
|
|
|
|
|
|
<strong class="table-text" :title="record.site_name">
|
|
|
|
|
|
{{ record.site_name }}
|
|
|
|
|
|
</strong>
|
|
|
|
|
|
<small
|
|
|
|
|
|
v-if="
|
|
|
|
|
|
record.content_citation_count > 0 &&
|
|
|
|
|
|
supportsContentCitationDisplay(record.ai_platform_id)
|
|
|
|
|
|
"
|
|
|
|
|
|
class="meta-subtext"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{
|
|
|
|
|
|
t('tracking.contentCitationMeta', {
|
|
|
|
|
|
count: record.content_citation_count,
|
|
|
|
|
|
})
|
|
|
|
|
|
}}
|
|
|
|
|
|
</small>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
<template v-else-if="column.key === 'domain'">
|
|
|
|
|
|
<span class="tracking-question-table__secondary">
|
|
|
|
|
|
{{ record.site_domain || record.site_key }}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
<template v-else-if="column.key === 'citationCount'">
|
|
|
|
|
|
<span class="metric-value">{{ record.citation_count }}</span>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
<template v-else-if="column.key === 'citationRate'">
|
|
|
|
|
|
<strong class="metric-highlight">
|
|
|
|
|
|
{{ formatPercent(record.citation_rate) }}
|
|
|
|
|
|
</strong>
|
|
|
|
|
|
</template>
|
2026-04-23 09:11:53 +08:00
|
|
|
|
</template>
|
2026-05-01 20:39:09 +08:00
|
|
|
|
</a-table>
|
|
|
|
|
|
<a-empty
|
|
|
|
|
|
v-else
|
|
|
|
|
|
:description="t('tracking.noCitationAnalysis')"
|
|
|
|
|
|
class="tracking-question-empty"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Content Citations -->
|
|
|
|
|
|
<section v-if="shouldShowInlineContentCitations" class="tracking-question-card">
|
|
|
|
|
|
<div class="tracking-question-card__header">
|
|
|
|
|
|
<div class="tracking-question-card__section-title">
|
|
|
|
|
|
<span class="tracking-question-card__accent" />
|
|
|
|
|
|
<span>{{ t('tracking.contentCitationsTitle') }}</span>
|
|
|
|
|
|
</div>
|
2026-04-12 09:56:18 +08:00
|
|
|
|
</div>
|
2026-05-01 20:39:09 +08:00
|
|
|
|
|
|
|
|
|
|
<div
|
|
|
|
|
|
class="tracking-question-card__scroll-area custom-scrollbar tracking-question-table-wrap"
|
2026-04-23 09:11:53 +08:00
|
|
|
|
>
|
2026-05-01 20:39:09 +08:00
|
|
|
|
<a-table
|
|
|
|
|
|
row-key="key"
|
|
|
|
|
|
class="modern-table"
|
|
|
|
|
|
:columns="contentCitationColumns"
|
|
|
|
|
|
:data-source="activeInlineContentCitations"
|
|
|
|
|
|
:pagination="false"
|
|
|
|
|
|
size="middle"
|
|
|
|
|
|
>
|
|
|
|
|
|
<template #bodyCell="{ column, record }">
|
|
|
|
|
|
<template v-if="column.key === 'contentName'">
|
|
|
|
|
|
<div class="cell-primary-stack">
|
|
|
|
|
|
<a
|
|
|
|
|
|
v-if="record.citedURL"
|
|
|
|
|
|
:href="record.citedURL"
|
|
|
|
|
|
target="_blank"
|
|
|
|
|
|
rel="noreferrer"
|
|
|
|
|
|
:title="record.contentName"
|
|
|
|
|
|
class="table-link"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ record.contentName }}
|
|
|
|
|
|
</a>
|
|
|
|
|
|
<strong v-else class="table-text" :title="record.contentName">
|
|
|
|
|
|
{{ record.contentName }}
|
|
|
|
|
|
</strong>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
<template v-else-if="column.key === 'channelName'">
|
|
|
|
|
|
<span class="tracking-question-table__secondary" :title="record.channelName">
|
|
|
|
|
|
{{ record.channelName }}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
<template v-else-if="column.key === 'citationCount'">
|
|
|
|
|
|
<span class="metric-value">{{ record.citationCount }}</span>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
<template v-else-if="column.key === 'citationRate'">
|
|
|
|
|
|
<strong class="metric-highlight">
|
|
|
|
|
|
{{ formatPercent(record.citationRate) }}
|
|
|
|
|
|
</strong>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
<template v-else-if="column.key === 'actions'">
|
|
|
|
|
|
<a-tooltip v-if="record.citedURL" :title="t('tracking.imitationAction')">
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
class="tracking-question-imitation-trigger tracking-question-imitation-trigger--table-action"
|
|
|
|
|
|
@click.stop="openImitationCreate(record.citedURL, record.contentName)"
|
|
|
|
|
|
>
|
|
|
|
|
|
<CopyOutlined />
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</a-tooltip>
|
|
|
|
|
|
<span v-else class="tracking-question-table__empty-action">--</span>
|
|
|
|
|
|
</template>
|
2026-04-25 21:36:44 +08:00
|
|
|
|
</template>
|
2026-05-01 20:39:09 +08:00
|
|
|
|
</a-table>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
</div>
|
2026-04-12 09:56:18 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</a-spin>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
|
.tracking-question-page {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 20px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tracking-question-page__content {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 20px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tracking-question-page__hero {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 16px;
|
|
|
|
|
|
padding: 24px 32px;
|
|
|
|
|
|
background: #ffffff;
|
|
|
|
|
|
border: 1px solid #e8edf5;
|
|
|
|
|
|
border-radius: 20px;
|
|
|
|
|
|
box-shadow: 0 4px 12px rgba(15, 23, 42, 0.02);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tracking-question-page__header-top {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tracking-question-page__back {
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
gap: 6px;
|
|
|
|
|
|
height: 32px;
|
|
|
|
|
|
padding: 0 12px 0 8px;
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
|
background: transparent;
|
|
|
|
|
|
color: #64748b;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
transition: all 0.2s ease;
|
|
|
|
|
|
margin-left: -8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tracking-question-page__back:hover {
|
|
|
|
|
|
background: #e2e8f0;
|
|
|
|
|
|
color: #0f172a;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tracking-question-page__copy {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
min-width: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tracking-question-page__eyebrow {
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
color: #2563eb;
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
|
letter-spacing: 0.1em;
|
|
|
|
|
|
text-transform: uppercase;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tracking-question-page__copy h1 {
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
color: #0f172a;
|
|
|
|
|
|
font-size: 32px;
|
|
|
|
|
|
font-weight: 800;
|
|
|
|
|
|
line-height: 1.25;
|
|
|
|
|
|
letter-spacing: -0.02em;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tracking-question-page__meta {
|
|
|
|
|
|
margin: 8px 0 0;
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
padding: 6px 12px;
|
|
|
|
|
|
background: #f1f5f9;
|
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
|
color: #475569;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
width: fit-content;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tracking-question-page__alert {
|
|
|
|
|
|
border-radius: 16px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tracking-question-page__tabs-wrapper {
|
|
|
|
|
|
background: #ffffff;
|
|
|
|
|
|
padding: 4px 24px 0;
|
|
|
|
|
|
border-radius: 16px;
|
|
|
|
|
|
border: 1px solid #e8edf5;
|
|
|
|
|
|
box-shadow: 0 4px 12px rgba(15, 23, 42, 0.02);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tracking-question-tabs :deep(.ant-tabs-nav) {
|
|
|
|
|
|
margin-bottom: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tracking-question-tabs .is-muted {
|
|
|
|
|
|
opacity: 0.65;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tracking-question-grid {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: minmax(0, 1.45fr) minmax(0, 1fr);
|
|
|
|
|
|
grid-auto-rows: 800px;
|
|
|
|
|
|
gap: 20px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tracking-question-card {
|
|
|
|
|
|
min-width: 0;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
padding: 24px 28px;
|
|
|
|
|
|
border: 1px solid #e8edf5;
|
|
|
|
|
|
border-radius: 20px;
|
|
|
|
|
|
background: #ffffff;
|
|
|
|
|
|
box-shadow: 0 10px 30px rgba(15, 23, 42, 0.03);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tracking-question-card--answer {
|
|
|
|
|
|
/* removed min-height, grid-auto-rows handles this universally */
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-22 00:24:21 +08:00
|
|
|
|
.tracking-question-card--span-2 {
|
|
|
|
|
|
grid-column: 1 / -1;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-12 09:56:18 +08:00
|
|
|
|
.tracking-question-card__header {
|
|
|
|
|
|
margin-bottom: 20px;
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tracking-question-card__header--split {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: flex-start;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
gap: 16px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tracking-question-card__section-title {
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 14px;
|
|
|
|
|
|
color: #111827;
|
|
|
|
|
|
font-size: 20px;
|
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tracking-question-card__accent {
|
|
|
|
|
|
display: inline-block;
|
|
|
|
|
|
width: 6px;
|
|
|
|
|
|
height: 28px;
|
|
|
|
|
|
border-radius: 999px;
|
|
|
|
|
|
background: #2563eb;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tracking-question-card__hint {
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
max-width: 420px;
|
|
|
|
|
|
color: #94a3b8;
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
line-height: 1.6;
|
|
|
|
|
|
text-align: right;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tracking-question-card__body {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
min-height: 0;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tracking-question-card__scroll-area {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
min-height: 0;
|
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
|
margin-right: -12px;
|
|
|
|
|
|
padding-right: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-24 22:21:01 +08:00
|
|
|
|
.tracking-question-table-wrap {
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
padding: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tracking-question-empty {
|
|
|
|
|
|
margin-top: 40px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-12 09:56:18 +08:00
|
|
|
|
.tracking-question-answer__headline {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
gap: 16px;
|
|
|
|
|
|
margin-bottom: 18px;
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tracking-question-answer__content {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
min-height: 0;
|
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
|
margin-right: -12px;
|
|
|
|
|
|
padding-right: 12px;
|
|
|
|
|
|
margin-bottom: 18px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-26 10:18:35 +08:00
|
|
|
|
.tracking-question-answer__content :deep(.markdown-preview) {
|
|
|
|
|
|
color: #111827;
|
|
|
|
|
|
font-size: 15px;
|
|
|
|
|
|
line-height: 1.85;
|
|
|
|
|
|
overflow-wrap: anywhere;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tracking-question-answer__content :deep(.markdown-preview h1),
|
|
|
|
|
|
.tracking-question-answer__content :deep(.markdown-preview h2),
|
|
|
|
|
|
.tracking-question-answer__content :deep(.markdown-preview h3),
|
|
|
|
|
|
.tracking-question-answer__content :deep(.markdown-preview h4),
|
|
|
|
|
|
.tracking-question-answer__content :deep(.markdown-preview h5),
|
|
|
|
|
|
.tracking-question-answer__content :deep(.markdown-preview h6) {
|
|
|
|
|
|
margin: 20px 0 10px;
|
|
|
|
|
|
color: #0f172a;
|
|
|
|
|
|
line-height: 1.35;
|
|
|
|
|
|
letter-spacing: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tracking-question-answer__content :deep(.markdown-preview h1) {
|
|
|
|
|
|
font-size: 24px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tracking-question-answer__content :deep(.markdown-preview h2) {
|
|
|
|
|
|
font-size: 21px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tracking-question-answer__content :deep(.markdown-preview h3) {
|
|
|
|
|
|
font-size: 18px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tracking-question-answer__content :deep(.markdown-preview h4),
|
|
|
|
|
|
.tracking-question-answer__content :deep(.markdown-preview h5),
|
|
|
|
|
|
.tracking-question-answer__content :deep(.markdown-preview h6) {
|
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tracking-question-answer__content :deep(.markdown-preview p),
|
|
|
|
|
|
.tracking-question-answer__content :deep(.markdown-preview ul),
|
|
|
|
|
|
.tracking-question-answer__content :deep(.markdown-preview ol),
|
|
|
|
|
|
.tracking-question-answer__content :deep(.markdown-preview blockquote),
|
|
|
|
|
|
.tracking-question-answer__content :deep(.markdown-preview table) {
|
|
|
|
|
|
margin: 0 0 14px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tracking-question-answer__content :deep(.markdown-preview ul),
|
|
|
|
|
|
.tracking-question-answer__content :deep(.markdown-preview ol) {
|
|
|
|
|
|
padding-left: 22px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tracking-question-answer__content :deep(.markdown-preview li + li) {
|
|
|
|
|
|
margin-top: 6px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tracking-question-answer__content :deep(.markdown-preview strong) {
|
|
|
|
|
|
color: #0f172a;
|
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tracking-question-answer__content :deep(.markdown-preview hr) {
|
|
|
|
|
|
margin: 22px 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tracking-question-answer__content :deep(.markdown-preview table) {
|
|
|
|
|
|
display: block;
|
|
|
|
|
|
max-width: 100%;
|
|
|
|
|
|
overflow-x: auto;
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-12 09:56:18 +08:00
|
|
|
|
.custom-scrollbar::-webkit-scrollbar {
|
|
|
|
|
|
width: 6px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.custom-scrollbar::-webkit-scrollbar-track {
|
|
|
|
|
|
background: transparent;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.custom-scrollbar::-webkit-scrollbar-thumb {
|
|
|
|
|
|
background: #e2e8f0;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
|
|
|
|
|
|
background: #cbd5e1;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tracking-question-answer__headline h2 {
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
font-size: 22px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tracking-question-answer__meta {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
gap: 18px;
|
|
|
|
|
|
margin-top: auto;
|
|
|
|
|
|
padding-top: 18px;
|
|
|
|
|
|
color: #9ca3af;
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tracking-question-source-list,
|
|
|
|
|
|
.tracking-question-content-list {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 14px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tracking-question-source-card,
|
|
|
|
|
|
.tracking-question-content-card {
|
2026-04-25 21:36:44 +08:00
|
|
|
|
position: relative;
|
2026-04-12 09:56:18 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 6px;
|
|
|
|
|
|
padding: 12px 16px;
|
|
|
|
|
|
border: 1px solid #edf2f7;
|
|
|
|
|
|
border-radius: 10px;
|
|
|
|
|
|
background: #f8fbff;
|
2026-04-25 21:36:44 +08:00
|
|
|
|
cursor: pointer;
|
2026-04-12 09:56:18 +08:00
|
|
|
|
transition: all 0.2s ease;
|
|
|
|
|
|
text-decoration: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tracking-question-source-card:hover,
|
|
|
|
|
|
.tracking-question-content-card:hover {
|
|
|
|
|
|
border-color: #dbeafe;
|
|
|
|
|
|
box-shadow: 0 4px 12px rgba(37, 99, 235, 0.05);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-25 21:36:44 +08:00
|
|
|
|
.tracking-question-source-card {
|
|
|
|
|
|
padding-right: 58px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-23 09:11:53 +08:00
|
|
|
|
.tracking-question-source-card.is-linked {
|
|
|
|
|
|
border-color: #93c5fd;
|
|
|
|
|
|
background: #eef6ff;
|
|
|
|
|
|
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.12);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-12 09:56:18 +08:00
|
|
|
|
.tracking-question-source-card__headline {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-23 22:54:56 +08:00
|
|
|
|
.tracking-question-source-card__indexes {
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 6px;
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-23 09:11:53 +08:00
|
|
|
|
.tracking-question-source-card__index {
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
min-width: 34px;
|
|
|
|
|
|
height: 24px;
|
|
|
|
|
|
padding: 0 8px;
|
|
|
|
|
|
border-radius: 999px;
|
|
|
|
|
|
background: #e8f1ff;
|
|
|
|
|
|
color: #1d4ed8;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-23 22:54:56 +08:00
|
|
|
|
.tracking-question-source-card__index--secondary {
|
|
|
|
|
|
background: #f8fafc;
|
|
|
|
|
|
color: #64748b;
|
|
|
|
|
|
border: 1px solid #e2e8f0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tracking-question-source-card__reference-badge {
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
min-width: 34px;
|
|
|
|
|
|
height: 24px;
|
|
|
|
|
|
padding: 0 8px;
|
|
|
|
|
|
border-radius: 999px;
|
|
|
|
|
|
border: 1px solid #bfdbfe;
|
|
|
|
|
|
background: #eff6ff;
|
|
|
|
|
|
color: #2563eb;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
|
line-height: 1;
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-12 09:56:18 +08:00
|
|
|
|
.tracking-question-source-card__headline strong {
|
|
|
|
|
|
display: block;
|
|
|
|
|
|
color: #2563eb;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tracking-question-source-card__divider {
|
|
|
|
|
|
color: #cbd5e1;
|
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tracking-question-source-card__title {
|
|
|
|
|
|
color: #475569;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tracking-question-source-card__badge {
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
margin-left: auto;
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-25 21:36:44 +08:00
|
|
|
|
.tracking-question-imitation-trigger {
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
width: 28px;
|
|
|
|
|
|
height: 28px;
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
border: 1px solid #dbeafe;
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
background: #ffffff;
|
|
|
|
|
|
color: #2563eb;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
opacity: 0;
|
|
|
|
|
|
transform: translateY(1px);
|
|
|
|
|
|
transition:
|
|
|
|
|
|
opacity 0.18s ease,
|
|
|
|
|
|
transform 0.18s ease,
|
|
|
|
|
|
border-color 0.18s ease,
|
|
|
|
|
|
background-color 0.18s ease;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tracking-question-imitation-trigger:hover {
|
|
|
|
|
|
border-color: #93c5fd;
|
|
|
|
|
|
background: #eff6ff;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tracking-question-imitation-trigger--source {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
top: 50%;
|
|
|
|
|
|
right: 16px;
|
|
|
|
|
|
transform: translateY(calc(-50% + 1px));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tracking-question-source-card:hover .tracking-question-imitation-trigger,
|
|
|
|
|
|
.tracking-question-source-card:focus-within .tracking-question-imitation-trigger {
|
|
|
|
|
|
opacity: 1;
|
|
|
|
|
|
transform: translateY(0);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tracking-question-source-card:hover .tracking-question-imitation-trigger--source,
|
|
|
|
|
|
.tracking-question-source-card:focus-within .tracking-question-imitation-trigger--source {
|
|
|
|
|
|
opacity: 1;
|
|
|
|
|
|
transform: translateY(-50%);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tracking-question-imitation-trigger--table-action {
|
|
|
|
|
|
margin: 0 auto;
|
|
|
|
|
|
opacity: 1;
|
|
|
|
|
|
transform: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tracking-question-table__empty-action {
|
|
|
|
|
|
color: #cbd5e1;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-12 09:56:18 +08:00
|
|
|
|
.tracking-question-source-card__link {
|
|
|
|
|
|
color: #94a3b8;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tracking-question-content-card__copy a {
|
|
|
|
|
|
color: #2563eb;
|
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-23 09:11:53 +08:00
|
|
|
|
.tracking-question-answer__content :deep(.tracking-inline-citation) {
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
min-width: 28px;
|
|
|
|
|
|
height: 22px;
|
|
|
|
|
|
margin-left: 6px;
|
|
|
|
|
|
padding: 0 8px;
|
|
|
|
|
|
border: 1px solid #bfdbfe;
|
|
|
|
|
|
border-radius: 999px;
|
|
|
|
|
|
background: #eff6ff;
|
|
|
|
|
|
color: #2563eb;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
|
line-height: 1;
|
|
|
|
|
|
vertical-align: baseline;
|
|
|
|
|
|
cursor: pointer;
|
2026-04-23 22:54:56 +08:00
|
|
|
|
text-decoration: none;
|
2026-04-23 09:11:53 +08:00
|
|
|
|
transition: all 0.18s ease;
|
2026-04-12 09:56:18 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-23 09:11:53 +08:00
|
|
|
|
.tracking-question-answer__content :deep(.tracking-inline-citation:hover) {
|
|
|
|
|
|
border-color: #93c5fd;
|
|
|
|
|
|
background: #dbeafe;
|
2026-04-12 09:56:18 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-23 09:11:53 +08:00
|
|
|
|
.tracking-question-answer__content :deep(.tracking-inline-citation--muted) {
|
|
|
|
|
|
cursor: default;
|
|
|
|
|
|
color: #94a3b8;
|
|
|
|
|
|
border-color: #e2e8f0;
|
|
|
|
|
|
background: #f8fafc;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Modern Enterprise Table Styles */
|
|
|
|
|
|
:deep(.modern-table) {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
:deep(.modern-table .ant-table-thead > tr > th) {
|
|
|
|
|
|
background-color: transparent !important;
|
|
|
|
|
|
color: #64748b;
|
|
|
|
|
|
font-weight: 600;
|
2026-04-12 09:56:18 +08:00
|
|
|
|
font-size: 13px;
|
2026-04-23 09:11:53 +08:00
|
|
|
|
border-bottom: 1px solid #e2e8f0;
|
|
|
|
|
|
padding: 16px 24px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
:deep(.modern-table .ant-table-tbody > tr > td) {
|
|
|
|
|
|
padding: 16px 24px;
|
|
|
|
|
|
border-bottom: 1px solid #f1f5f9;
|
|
|
|
|
|
vertical-align: middle;
|
|
|
|
|
|
transition: background-color 0.2s ease;
|
2026-04-12 09:56:18 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-23 09:11:53 +08:00
|
|
|
|
:deep(.modern-table .ant-table-tbody > tr:last-child > td) {
|
|
|
|
|
|
border-bottom: none;
|
2026-04-12 09:56:18 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-23 09:11:53 +08:00
|
|
|
|
:deep(.modern-table .ant-table-tbody > tr:hover > td) {
|
|
|
|
|
|
background-color: #f8fafc !important;
|
2026-04-12 09:56:18 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-23 09:11:53 +08:00
|
|
|
|
.cell-primary-stack {
|
2026-04-12 09:56:18 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
2026-04-23 09:11:53 +08:00
|
|
|
|
gap: 4px;
|
2026-04-12 09:56:18 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-23 09:11:53 +08:00
|
|
|
|
.table-link {
|
|
|
|
|
|
color: #2563eb;
|
|
|
|
|
|
font-size: 14px;
|
2026-04-12 09:56:18 +08:00
|
|
|
|
font-weight: 600;
|
2026-04-23 09:11:53 +08:00
|
|
|
|
text-decoration: none;
|
|
|
|
|
|
transition: color 0.2s ease;
|
|
|
|
|
|
display: -webkit-box;
|
|
|
|
|
|
-webkit-line-clamp: 2;
|
|
|
|
|
|
line-clamp: 2;
|
|
|
|
|
|
-webkit-box-orient: vertical;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
text-overflow: ellipsis;
|
2026-04-12 09:56:18 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-23 09:11:53 +08:00
|
|
|
|
.table-link:hover {
|
|
|
|
|
|
color: #1d4ed8;
|
|
|
|
|
|
text-decoration: underline;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.table-text {
|
|
|
|
|
|
color: #0f172a;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
display: -webkit-box;
|
|
|
|
|
|
-webkit-line-clamp: 2;
|
|
|
|
|
|
line-clamp: 2;
|
|
|
|
|
|
-webkit-box-orient: vertical;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
text-overflow: ellipsis;
|
2026-04-12 09:56:18 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tracking-question-table__secondary {
|
2026-04-23 09:11:53 +08:00
|
|
|
|
color: #64748b;
|
|
|
|
|
|
font-size: 13px;
|
2026-04-12 09:56:18 +08:00
|
|
|
|
word-break: break-all;
|
2026-04-23 09:11:53 +08:00
|
|
|
|
display: -webkit-box;
|
|
|
|
|
|
-webkit-line-clamp: 2;
|
|
|
|
|
|
line-clamp: 2;
|
|
|
|
|
|
-webkit-box-orient: vertical;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
text-overflow: ellipsis;
|
2026-04-12 09:56:18 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-23 09:11:53 +08:00
|
|
|
|
.meta-subtext {
|
|
|
|
|
|
color: #94a3b8;
|
|
|
|
|
|
font-size: 12px;
|
2026-04-12 09:56:18 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-23 09:11:53 +08:00
|
|
|
|
.metric-value {
|
|
|
|
|
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
|
|
|
|
|
color: #475569;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
font-size: 15px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.metric-highlight {
|
|
|
|
|
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
|
|
|
|
|
color: #0ea5e9;
|
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
background: #f0f9ff;
|
|
|
|
|
|
padding: 4px 10px;
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
border: 1px solid #e0f2fe;
|
2026-04-12 09:56:18 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tracking-question-card--debug {
|
|
|
|
|
|
margin-top: 4px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tracking-task-debug-list {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 16px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tracking-task-debug-card {
|
|
|
|
|
|
padding: 20px 22px;
|
|
|
|
|
|
border: 1px solid #edf2f7;
|
|
|
|
|
|
border-radius: 20px;
|
|
|
|
|
|
background: linear-gradient(180deg, #fbfdff 0%, #f5f9ff 100%);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tracking-task-debug-card__header {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: flex-start;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
gap: 16px;
|
|
|
|
|
|
margin-bottom: 18px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tracking-task-debug-card__headline {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
min-width: 0;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 6px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tracking-task-debug-card__headline strong {
|
|
|
|
|
|
color: #111827;
|
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tracking-task-debug-card__headline small {
|
|
|
|
|
|
color: #94a3b8;
|
|
|
|
|
|
word-break: break-all;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tracking-task-debug-card__tags {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
justify-content: flex-end;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tracking-task-debug-grid {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
|
|
|
|
gap: 14px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tracking-task-debug-grid__item {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
min-width: 0;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
padding: 14px 16px;
|
|
|
|
|
|
border-radius: 16px;
|
|
|
|
|
|
background: #ffffff;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tracking-task-debug-grid__item span {
|
|
|
|
|
|
color: #94a3b8;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tracking-task-debug-grid__item strong {
|
|
|
|
|
|
color: #1f2937;
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
line-height: 1.6;
|
|
|
|
|
|
word-break: break-all;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tracking-task-debug-notes {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
|
|
|
|
gap: 14px;
|
|
|
|
|
|
margin-top: 14px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tracking-task-debug-notes__item {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
padding: 14px 16px;
|
|
|
|
|
|
border-radius: 16px;
|
|
|
|
|
|
background: #ffffff;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tracking-task-debug-notes__item span {
|
|
|
|
|
|
color: #94a3b8;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tracking-task-debug-notes__item strong {
|
|
|
|
|
|
color: #1f2937;
|
|
|
|
|
|
line-height: 1.6;
|
|
|
|
|
|
word-break: break-word;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tracking-task-debug-notes__item.is-error {
|
|
|
|
|
|
background: #fff7f7;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tracking-task-debug-notes__item.is-error strong {
|
|
|
|
|
|
color: #b91c1c;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@media (max-width: 1080px) {
|
|
|
|
|
|
.tracking-question-grid {
|
|
|
|
|
|
grid-template-columns: 1fr;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tracking-task-debug-grid,
|
|
|
|
|
|
.tracking-task-debug-notes {
|
|
|
|
|
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@media (max-width: 720px) {
|
|
|
|
|
|
.tracking-question-page__hero {
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tracking-question-page__copy h1 {
|
|
|
|
|
|
font-size: 26px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tracking-question-card {
|
|
|
|
|
|
padding: 22px 18px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tracking-question-card__header--split,
|
|
|
|
|
|
.tracking-task-debug-card__header {
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tracking-question-card__hint,
|
|
|
|
|
|
.tracking-task-debug-card__tags {
|
|
|
|
|
|
max-width: none;
|
|
|
|
|
|
text-align: left;
|
|
|
|
|
|
justify-content: flex-start;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tracking-question-table__head,
|
|
|
|
|
|
.tracking-question-table__row {
|
|
|
|
|
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tracking-question-source-card,
|
|
|
|
|
|
.tracking-question-content-card {
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
align-items: flex-start;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-23 09:11:53 +08:00
|
|
|
|
.tracking-question-source-card__meta {
|
2026-04-12 09:56:18 +08:00
|
|
|
|
align-items: flex-start;
|
|
|
|
|
|
justify-content: flex-start;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tracking-task-debug-grid,
|
|
|
|
|
|
.tracking-task-debug-notes {
|
|
|
|
|
|
grid-template-columns: 1fr;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|