|
|
|
@@ -4,7 +4,6 @@ import { useQuery } from "@tanstack/vue-query";
|
|
|
|
|
import dayjs from "dayjs";
|
|
|
|
|
import type {
|
|
|
|
|
MonitoringQuestionCitationAnalysisItem,
|
|
|
|
|
MonitoringQuestionContentCitation,
|
|
|
|
|
MonitoringQuestionDetailCitation,
|
|
|
|
|
MonitoringQuestionDetailPlatform,
|
|
|
|
|
} from "@geo/shared-types";
|
|
|
|
@@ -98,11 +97,6 @@ const activeCitationAnalysis = computed<MonitoringQuestionCitationAnalysisItem[]
|
|
|
|
|
return items.filter((item) => item.ai_platform_id === activePlatformId.value);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const activeContentCitations = computed<MonitoringQuestionContentCitation[]>(() => {
|
|
|
|
|
const items = detailQuery.data.value?.content_citations ?? [];
|
|
|
|
|
return items.filter((item) => item.ai_platform_id === activePlatformId.value);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const latestSampledAt = computed<string | null>(() => {
|
|
|
|
|
const platforms = detailQuery.data.value?.platforms ?? [];
|
|
|
|
|
const sampledAtValues = platforms
|
|
|
|
@@ -126,6 +120,27 @@ const collectedAtDisplay = computed(() => {
|
|
|
|
|
return sampledAt ? formatDateTime(sampledAt) : "--";
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
type TrackingAnalyzedContentCitation = {
|
|
|
|
|
key: string;
|
|
|
|
|
contentName: string;
|
|
|
|
|
channelName: string;
|
|
|
|
|
citationCount: number;
|
|
|
|
|
citationRate: number | null;
|
|
|
|
|
citedURL: string | null;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const sanitizedAnswerText = computed<string | null>(() => {
|
|
|
|
|
return stripAnswerCitationMarkers(activePlatform.value?.answer_text);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const activeInlineContentCitations = computed<TrackingAnalyzedContentCitation[]>(() => {
|
|
|
|
|
return analyzeInlineContentCitations(activePlatform.value);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const shouldShowInlineContentCitations = computed(() => {
|
|
|
|
|
return activeInlineContentCitations.value.length > 0;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
function goBack(): void {
|
|
|
|
|
void router.push({
|
|
|
|
|
name: "tracking",
|
|
|
|
@@ -168,6 +183,74 @@ function resolveCitationTitle(citation: MonitoringQuestionDetailCitation): strin
|
|
|
|
|
return citation.cited_title || citation.article_title || citation.cited_url;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function createQwenSourceGroupPattern(): RegExp {
|
|
|
|
|
return /\[\[source_group_web_(\d+)\]\]/g;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function stripAnswerCitationMarkers(answerText: string | null | undefined): string | null {
|
|
|
|
|
const normalized = String(answerText ?? "").trim();
|
|
|
|
|
if (!normalized) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const cleaned = normalized
|
|
|
|
|
.replace(createQwenSourceGroupPattern(), "")
|
|
|
|
|
.replace(/[ \t]+([,.;:!?,。!?;:])/g, "$1")
|
|
|
|
|
.replace(/[ \t]{2,}/g, " ")
|
|
|
|
|
.replace(/\n{3,}/g, "\n\n")
|
|
|
|
|
.trim();
|
|
|
|
|
|
|
|
|
|
return cleaned || null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function analyzeInlineContentCitations(
|
|
|
|
|
platform: MonitoringQuestionDetailPlatform | null,
|
|
|
|
|
): TrackingAnalyzedContentCitation[] {
|
|
|
|
|
const answerText = String(platform?.answer_text ?? "");
|
|
|
|
|
if (!answerText) {
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const matches = Array.from(answerText.matchAll(createQwenSourceGroupPattern()));
|
|
|
|
|
if (!matches.length) {
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const citationCounts = new Map<number, number>();
|
|
|
|
|
for (const match of matches) {
|
|
|
|
|
const sourceGroupIndex = Number(match[1]);
|
|
|
|
|
if (!Number.isFinite(sourceGroupIndex) || sourceGroupIndex <= 0) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
citationCounts.set(sourceGroupIndex, (citationCounts.get(sourceGroupIndex) ?? 0) + 1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const totalCitationCount = Array.from(citationCounts.values()).reduce((sum, count) => sum + count, 0);
|
|
|
|
|
if (!totalCitationCount) {
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return Array.from(citationCounts.entries())
|
|
|
|
|
.sort((left, right) => {
|
|
|
|
|
if (right[1] !== left[1]) {
|
|
|
|
|
return right[1] - left[1];
|
|
|
|
|
}
|
|
|
|
|
return left[0] - right[0];
|
|
|
|
|
})
|
|
|
|
|
.map(([sourceGroupIndex, citationCount]) => {
|
|
|
|
|
const citation = platform?.citations[sourceGroupIndex - 1] ?? null;
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
key: `${sourceGroupIndex}-${citation?.cited_url ?? `source_group_web_${sourceGroupIndex}`}`,
|
|
|
|
|
contentName: citation ? resolveCitationTitle(citation) : `引用内容 #${sourceGroupIndex}`,
|
|
|
|
|
channelName: citation?.site_name ?? "--",
|
|
|
|
|
citationCount,
|
|
|
|
|
citationRate: citationCount / totalCitationCount,
|
|
|
|
|
citedURL: citation?.cited_url ?? null,
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function normalizeQueryValue(value: unknown): string {
|
|
|
|
|
const raw = Array.isArray(value) ? value[0] : value;
|
|
|
|
|
return String(raw ?? "").trim();
|
|
|
|
@@ -268,8 +351,8 @@ function parsePositiveNumber(value: unknown): number | null {
|
|
|
|
|
|
|
|
|
|
<div class="tracking-question-answer__content custom-scrollbar">
|
|
|
|
|
<MarkdownPreview
|
|
|
|
|
v-if="activePlatform?.answer_text"
|
|
|
|
|
:content="activePlatform.answer_text"
|
|
|
|
|
v-if="sanitizedAnswerText"
|
|
|
|
|
:content="sanitizedAnswerText"
|
|
|
|
|
/>
|
|
|
|
|
<a-empty v-else :description="t('tracking.noAnswer')" />
|
|
|
|
|
</div>
|
|
|
|
@@ -314,7 +397,12 @@ function parsePositiveNumber(value: unknown): number | null {
|
|
|
|
|
</div>
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
<section class="tracking-question-card">
|
|
|
|
|
<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" />
|
|
|
|
@@ -351,7 +439,7 @@ function parsePositiveNumber(value: unknown): number | null {
|
|
|
|
|
</div>
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
<section class="tracking-question-card">
|
|
|
|
|
<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" />
|
|
|
|
@@ -360,34 +448,40 @@ function parsePositiveNumber(value: unknown): number | null {
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="tracking-question-card__scroll-area custom-scrollbar">
|
|
|
|
|
<div v-if="activeContentCitations.length" class="tracking-question-table">
|
|
|
|
|
<div
|
|
|
|
|
class="tracking-question-table__head"
|
|
|
|
|
>
|
|
|
|
|
<div class="tracking-question-table">
|
|
|
|
|
<div class="tracking-question-table__head">
|
|
|
|
|
<span>{{ t("tracking.columns.contentName") }}</span>
|
|
|
|
|
<span>{{ t("tracking.columns.publishPlatform") }}</span>
|
|
|
|
|
<span>{{ t("tracking.columns.channelName") }}</span>
|
|
|
|
|
<span>{{ t("tracking.columns.citations") }}</span>
|
|
|
|
|
<span>{{ t("tracking.columns.share") }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div
|
|
|
|
|
v-for="item in activeContentCitations"
|
|
|
|
|
:key="`${item.ai_platform_id}-${item.article_id}`"
|
|
|
|
|
v-for="item in activeInlineContentCitations"
|
|
|
|
|
:key="item.key"
|
|
|
|
|
class="tracking-question-table__row"
|
|
|
|
|
>
|
|
|
|
|
<div class="tracking-question-table__primary">
|
|
|
|
|
<router-link :to="`/articles/${item.article_id}/edit`">{{ item.article_title }}</router-link>
|
|
|
|
|
<a
|
|
|
|
|
v-if="item.citedURL"
|
|
|
|
|
:href="item.citedURL"
|
|
|
|
|
target="_blank"
|
|
|
|
|
rel="noreferrer"
|
|
|
|
|
:title="item.contentName"
|
|
|
|
|
>
|
|
|
|
|
{{ item.contentName }}
|
|
|
|
|
</a>
|
|
|
|
|
<strong v-else>{{ item.contentName }}</strong>
|
|
|
|
|
</div>
|
|
|
|
|
<span class="tracking-question-table__secondary">{{ item.publish_platform }}</span>
|
|
|
|
|
<span class="tracking-question-table__secondary">{{ item.channelName }}</span>
|
|
|
|
|
<div class="tracking-question-table__metric">
|
|
|
|
|
<span>{{ item.citation_count }}</span>
|
|
|
|
|
<span>{{ item.citationCount }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="tracking-question-table__metric">
|
|
|
|
|
<strong>{{ formatPercent(item.citation_rate) }}</strong>
|
|
|
|
|
<strong>{{ formatPercent(item.citationRate) }}</strong>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<a-empty v-else :description="t('tracking.noContentCitations')" />
|
|
|
|
|
</div>
|
|
|
|
|
</section>
|
|
|
|
|
</div>
|
|
|
|
@@ -528,6 +622,10 @@ function parsePositiveNumber(value: unknown): number | null {
|
|
|
|
|
/* removed min-height, grid-auto-rows handles this universally */
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.tracking-question-card--span-2 {
|
|
|
|
|
grid-column: 1 / -1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.tracking-question-card__header {
|
|
|
|
|
margin-bottom: 20px;
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|