feat(mobile): show new ask sources and direct citation navigation
- citation chips: 正式经验 (CASE_EXP) / 同事经验·有奖征答 (COMMUNITY) - citation navigation: CASE_EXP opens case detail via ?caseId=, COMMUNITY goes straight to question detail via community-question detailRef, falling back to the question list - disable human-help action when the answer was already marked helpful (server would 409)
This commit is contained in:
@@ -140,7 +140,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
import { onShow, onUnload } from '@dcloudio/uni-app';
|
import { onLoad, onShow, onUnload } from '@dcloudio/uni-app';
|
||||||
import type { CaseCurateResponse, CaseDetailResponse, CaseOrganizeResponse, CaseRecord, CaseSelectedFile, CaseUploadResponse } from '@/types/api';
|
import type { CaseCurateResponse, CaseDetailResponse, CaseOrganizeResponse, CaseRecord, CaseSelectedFile, CaseUploadResponse } from '@/types/api';
|
||||||
import { chooseCaseAudio, curateCase, getCaseCapabilities, getCaseDetail, getCaseMediaUrl, getCaseRecords, organizeCase, uploadCaseAudio } from '@/services/case';
|
import { chooseCaseAudio, curateCase, getCaseCapabilities, getCaseDetail, getCaseMediaUrl, getCaseRecords, organizeCase, uploadCaseAudio } from '@/services/case';
|
||||||
import { releaseAuthenticatedFileUrl } from '@/services/api';
|
import { releaseAuthenticatedFileUrl } from '@/services/api';
|
||||||
@@ -164,6 +164,8 @@ const status = ref<'idle' | 'uploading' | 'organizing' | 'curating'>('idle');
|
|||||||
const recordsLoading = ref(false);
|
const recordsLoading = ref(false);
|
||||||
const caseDetailLoading = ref(false);
|
const caseDetailLoading = ref(false);
|
||||||
const message = ref('');
|
const message = ref('');
|
||||||
|
/** 引用跳转带入的 caseId:加载完列表后自动打开对应详情,消费一次即清空。 */
|
||||||
|
const pendingCaseId = ref('');
|
||||||
const busy = computed(() => status.value !== 'idle');
|
const busy = computed(() => status.value !== 'idle');
|
||||||
const statusText = computed(() => {
|
const statusText = computed(() => {
|
||||||
if (status.value === 'uploading') return '转写中';
|
if (status.value === 'uploading') return '转写中';
|
||||||
@@ -229,6 +231,11 @@ const loadRecords = async () => {
|
|||||||
recordsLoading.value = true;
|
recordsLoading.value = true;
|
||||||
try {
|
try {
|
||||||
records.value = await getCaseRecords();
|
records.value = await getCaseRecords();
|
||||||
|
if (pendingCaseId.value) {
|
||||||
|
const caseId = pendingCaseId.value;
|
||||||
|
pendingCaseId.value = '';
|
||||||
|
void openCaseDetail({ caseId });
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleRequestError(error, '最近案例加载失败', true);
|
handleRequestError(error, '最近案例加载失败', true);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -258,7 +265,7 @@ const refreshRecords = () => {
|
|||||||
void loadRecords();
|
void loadRecords();
|
||||||
};
|
};
|
||||||
|
|
||||||
const openCaseDetail = async (record: CaseRecord) => {
|
const openCaseDetail = async (record: Pick<CaseRecord, 'caseId'>) => {
|
||||||
if (!record.caseId || caseDetailLoading.value) return;
|
if (!record.caseId || caseDetailLoading.value) return;
|
||||||
caseDetailLoading.value = true;
|
caseDetailLoading.value = true;
|
||||||
caseMediaLoading.value = true;
|
caseMediaLoading.value = true;
|
||||||
@@ -332,6 +339,10 @@ const handleShow = async () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
onLoad((options) => {
|
||||||
|
pendingCaseId.value = typeof options?.caseId === 'string' && options.caseId ? options.caseId : '';
|
||||||
|
});
|
||||||
|
|
||||||
onShow(() => {
|
onShow(() => {
|
||||||
void handleShow();
|
void handleShow();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -263,12 +263,12 @@
|
|||||||
|
|
||||||
<view v-if="msg.citationsOpen && msg.result.snippets?.length" class="bubble-citations">
|
<view v-if="msg.citationsOpen && msg.result.snippets?.length" class="bubble-citations">
|
||||||
<component
|
<component
|
||||||
:is="snippet.detailRef && snippet.sourceType === 'DOCUMENT' ? 'button' : 'view'"
|
:is="citationClickable(snippet) ? 'button' : 'view'"
|
||||||
v-for="(snippet, index) in msg.result.snippets"
|
v-for="(snippet, index) in msg.result.snippets"
|
||||||
:key="`${snippet.docTitle}-${index}`"
|
:key="`${snippet.docTitle}-${index}`"
|
||||||
class="citation"
|
class="citation"
|
||||||
:class="{ 'citation-action': Boolean(snippet.detailRef && snippet.sourceType === 'DOCUMENT') }"
|
:class="{ 'citation-action': citationClickable(snippet) }"
|
||||||
@click="openCitationDetail(snippet)"
|
@click="openCitation(snippet)"
|
||||||
>
|
>
|
||||||
<text class="citation-domain" :class="{ project: snippet.domain === 'PROJECT_SERVICE' }">
|
<text class="citation-domain" :class="{ project: snippet.domain === 'PROJECT_SERVICE' }">
|
||||||
{{ citationDomainLabel(snippet) }}
|
{{ citationDomainLabel(snippet) }}
|
||||||
@@ -283,6 +283,9 @@
|
|||||||
<text class="citation-location">{{ citationLocationLabel(snippet) }}</text>
|
<text class="citation-location">{{ citationLocationLabel(snippet) }}</text>
|
||||||
<text class="citation-open">查看出处 ›</text>
|
<text class="citation-open">查看出处 ›</text>
|
||||||
</view>
|
</view>
|
||||||
|
<view v-else-if="citationClickable(snippet)" class="citation-open-row">
|
||||||
|
<text class="citation-open">查看出处 ›</text>
|
||||||
|
</view>
|
||||||
</component>
|
</component>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
@@ -1355,6 +1358,8 @@ const citationDomainLabel = (snippet: KnowledgeSearchResponse['snippets'][number
|
|||||||
if (snippet.domain === 'PROJECT_SERVICE') return '项目记录';
|
if (snippet.domain === 'PROJECT_SERVICE') return '项目记录';
|
||||||
if (snippet.domain === 'PERSONAL') return '我的资料';
|
if (snippet.domain === 'PERSONAL') return '我的资料';
|
||||||
if (snippet.sourceType === 'DATA_TOOL') return '数据工具';
|
if (snippet.sourceType === 'DATA_TOOL') return '数据工具';
|
||||||
|
if (snippet.sourceType === 'CASE_EXP') return '正式经验';
|
||||||
|
if (snippet.sourceType === 'COMMUNITY') return '同事经验·有奖征答';
|
||||||
return ({
|
return ({
|
||||||
FORMAL_POLICY: '正式制度',
|
FORMAL_POLICY: '正式制度',
|
||||||
OPERATING_MANUAL: '操作手册',
|
OPERATING_MANUAL: '操作手册',
|
||||||
@@ -1395,6 +1400,47 @@ const openCitationDetail = (snippet: KnowledgeSearchResponse['snippets'][number]
|
|||||||
uni.navigateTo({ url: `/pages/user/sop/citation-detail?ref=${encodeURIComponent(snippet.detailRef)}` });
|
uni.navigateTo({ url: `/pages/user/sop/citation-detail?ref=${encodeURIComponent(snippet.detailRef)}` });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** 从引用字段解析新来源业务 ID:docId 的 case:{caseId} / community-answer:{answerId},或 detailRef 的 community-question:{questionId};解析不到即空串。 */
|
||||||
|
const citationSourceId = (
|
||||||
|
snippet: KnowledgeSearchResponse['snippets'][number],
|
||||||
|
prefix: string,
|
||||||
|
field: 'docId' | 'detailRef' = 'docId'
|
||||||
|
) => {
|
||||||
|
const value = (field === 'detailRef' ? snippet.detailRef : snippet.docId) || '';
|
||||||
|
return value.startsWith(prefix) ? value.slice(prefix.length) : '';
|
||||||
|
};
|
||||||
|
|
||||||
|
const citationClickable = (snippet: KnowledgeSearchResponse['snippets'][number]) => {
|
||||||
|
if (snippet.detailRef && snippet.sourceType === 'DOCUMENT') return true;
|
||||||
|
if (snippet.sourceType === 'CASE_EXP') return Boolean(citationSourceId(snippet, 'case:'));
|
||||||
|
if (snippet.sourceType === 'COMMUNITY') return true;
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 出处跳转:DOCUMENT 走 citation-detail;CASE_EXP 去案例页按 caseId 自动打开详情;COMMUNITY 按 detailRef 直达问题详情,无则回退列表。 */
|
||||||
|
const openCitation = (snippet: KnowledgeSearchResponse['snippets'][number]) => {
|
||||||
|
if (!citationClickable(snippet)) return;
|
||||||
|
if (snippet.detailRef && snippet.sourceType === 'DOCUMENT') {
|
||||||
|
openCitationDetail(snippet);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (snippet.sourceType === 'CASE_EXP') {
|
||||||
|
const caseId = citationSourceId(snippet, 'case:');
|
||||||
|
if (caseId) {
|
||||||
|
uni.navigateTo({ url: `/pages/user/cases/index?caseId=${encodeURIComponent(caseId)}` });
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (snippet.sourceType === 'COMMUNITY') {
|
||||||
|
const questionId = citationSourceId(snippet, 'community-question:', 'detailRef');
|
||||||
|
if (questionId) {
|
||||||
|
uni.navigateTo({ url: `/pages/user/questions/detail?id=${encodeURIComponent(questionId)}` });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
uni.navigateTo({ url: '/pages/user/questions/index' });
|
||||||
|
};
|
||||||
|
|
||||||
const noEvidenceText = (result: KnowledgeSearchResponse) => {
|
const noEvidenceText = (result: KnowledgeSearchResponse) => {
|
||||||
if (result.agentIntent === 'WEB_RESEARCH') return '公开网络中没有找到可核验来源。';
|
if (result.agentIntent === 'WEB_RESEARCH') return '公开网络中没有找到可核验来源。';
|
||||||
if (result.agentIntent === 'MEDIA_UNDERSTANDING') return '已分析附件,但当前资料中没有补充依据。';
|
if (result.agentIntent === 'MEDIA_UNDERSTANDING') return '已分析附件,但当前资料中没有补充依据。';
|
||||||
@@ -1437,7 +1483,9 @@ const nextActionLabel = (action: NextAction) => {
|
|||||||
|
|
||||||
const nextActionDisabled = (action: NextAction, msg: MasterMsg) => {
|
const nextActionDisabled = (action: NextAction, msg: MasterMsg) => {
|
||||||
if (action.type === 'HUMAN_HELP') {
|
if (action.type === 'HUMAN_HELP') {
|
||||||
return msg.helpSubmitting || msg.feedbackSubmitting || Boolean(msg.helpQuestionId) || !msg.result.requestId;
|
// 已点“有帮助”再转人工必被服务端 409,直接禁用避免死路。
|
||||||
|
return msg.helpSubmitting || msg.feedbackSubmitting || Boolean(msg.helpQuestionId) || !msg.result.requestId
|
||||||
|
|| msg.feedbackVerdict === 'up';
|
||||||
}
|
}
|
||||||
if (action.type === 'DIRECT_CHANNEL') return !action.channel;
|
if (action.type === 'DIRECT_CHANNEL') return !action.channel;
|
||||||
return false;
|
return false;
|
||||||
@@ -1447,6 +1495,7 @@ const nextActionButtonLabel = (action: NextAction, msg: MasterMsg) => {
|
|||||||
if (action.type === 'HUMAN_HELP') {
|
if (action.type === 'HUMAN_HELP') {
|
||||||
if (msg.helpSubmitting || msg.feedbackSubmitting) return '提交中';
|
if (msg.helpSubmitting || msg.feedbackSubmitting) return '提交中';
|
||||||
if (msg.helpQuestionId) return '已提交审核';
|
if (msg.helpQuestionId) return '已提交审核';
|
||||||
|
if (msg.feedbackVerdict === 'up') return '已反馈有帮助,如需求助请重新提问';
|
||||||
}
|
}
|
||||||
return nextActionLabel(action);
|
return nextActionLabel(action);
|
||||||
};
|
};
|
||||||
@@ -1516,6 +1565,7 @@ const submitAutoDownFeedback = async (msg: MasterMsg) => {
|
|||||||
/** nextActions 的 HUMAN_HELP 入口:未提交过 down 反馈时先自动补提交,再走既有确认弹窗 + 幂等提交流程。 */
|
/** nextActions 的 HUMAN_HELP 入口:未提交过 down 反馈时先自动补提交,再走既有确认弹窗 + 幂等提交流程。 */
|
||||||
const submitHumanHelpFromNextAction = async (msg: MasterMsg) => {
|
const submitHumanHelpFromNextAction = async (msg: MasterMsg) => {
|
||||||
if (msg.helpSubmitting || msg.helpQuestionId || !msg.result.requestId) return;
|
if (msg.helpSubmitting || msg.helpQuestionId || !msg.result.requestId) return;
|
||||||
|
if (msg.feedbackVerdict === 'up') return;
|
||||||
if (!msg.feedbackVerdict && !await submitAutoDownFeedback(msg)) return;
|
if (!msg.feedbackVerdict && !await submitAutoDownFeedback(msg)) return;
|
||||||
await submitHumanHelp(msg);
|
await submitHumanHelp(msg);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user