feat(mobile): add personal assistant knowledge flow
This commit is contained in:
@@ -0,0 +1,163 @@
|
||||
<template>
|
||||
<view class="page assistant-page employee-page-shell">
|
||||
<view class="subpage-header">
|
||||
<button class="subpage-back-button" @click="goBack">‹</button>
|
||||
<view class="subpage-heading">
|
||||
<text class="page-title">个人 AI 助理</text>
|
||||
<text class="page-subtitle">先查我的资料,需要时再主动加入企业知识</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="panel employee-card domain-panel">
|
||||
<text class="panel-title">回答范围</text>
|
||||
<view class="domain-options">
|
||||
<button class="domain-option active" disabled>
|
||||
<text class="domain-name">我的资料</text>
|
||||
<text class="domain-desc">个人收藏,仅自己可见</text>
|
||||
</button>
|
||||
<button class="domain-option" :class="{ active: enterpriseEnabled }" @click="toggleEnterprise">
|
||||
<text class="domain-name">企业知识</text>
|
||||
<text class="domain-desc">企业知识按岗位权限</text>
|
||||
</button>
|
||||
</view>
|
||||
<text class="scope-note">默认只查询个人资料;企业知识不会自动开启。</text>
|
||||
</view>
|
||||
|
||||
<view class="panel employee-card ask-panel">
|
||||
<textarea v-model="queryText" class="textarea" maxlength="1000" placeholder="例如:帮我汇总最近收藏的投诉处理要点" />
|
||||
<button class="primary-button ask-button" :disabled="asking || !queryText.trim()" @click="submitQuestion">
|
||||
{{ asking ? '正在查找依据' : '提问' }}
|
||||
</button>
|
||||
<text v-if="message" class="status-message">{{ message }}</text>
|
||||
</view>
|
||||
|
||||
<view class="shortcut-grid">
|
||||
<button class="shortcut employee-card" @click="openLibrary">
|
||||
<text class="shortcut-title">我的资料库</text>
|
||||
<text class="shortcut-desc">查看解析状态与来源</text>
|
||||
</button>
|
||||
<button class="shortcut employee-card" @click="openCapture">
|
||||
<text class="shortcut-title">收藏资料</text>
|
||||
<text class="shortcut-desc">文字、文件或网页</text>
|
||||
</button>
|
||||
<button class="shortcut employee-card" @click="openSessions">
|
||||
<text class="shortcut-title">历史会话</text>
|
||||
<text class="shortcut-desc">继续查看过往问答</text>
|
||||
</button>
|
||||
</view>
|
||||
|
||||
<view v-if="answer" class="panel employee-card answer-panel">
|
||||
<text class="panel-title">回答</text>
|
||||
<text class="answer-text">{{ answer.answer }}</text>
|
||||
<view v-if="answer.citations?.length" class="citation-list">
|
||||
<text class="citation-heading">引用依据</text>
|
||||
<button v-for="citation in answer.citations" :key="`${citation.domain}-${citation.sourceId}`" class="citation-card" @click="openCitation(citation)">
|
||||
<view class="citation-head">
|
||||
<text class="domain-badge" :class="citation.domain.toLowerCase()">{{ citationLabel(citation) }}</text>
|
||||
<text class="citation-title">{{ citation.title }}</text>
|
||||
</view>
|
||||
<text class="citation-excerpt">{{ citation.excerpt }}</text>
|
||||
</button>
|
||||
</view>
|
||||
<view v-else class="empty">本次回答没有可展示的引用,不补造来源。</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue';
|
||||
import type { PersonalAskResponse, PersonalCitation } from '@/types/api';
|
||||
import { askPersonalAssistant, loadPersonalItem, type KnowledgeScope } from '@/services/personal-assistant';
|
||||
|
||||
const scopes = ref<KnowledgeScope[]>(['PERSONAL']);
|
||||
const queryText = ref('');
|
||||
const asking = ref(false);
|
||||
const answer = ref<PersonalAskResponse>();
|
||||
const message = ref('');
|
||||
const citationSourceTypes = ref<Record<string, string>>({});
|
||||
const enterpriseEnabled = computed(() => scopes.value.includes('ENTERPRISE'));
|
||||
|
||||
const toggleEnterprise = () => {
|
||||
scopes.value = enterpriseEnabled.value ? ['PERSONAL'] : ['PERSONAL', 'ENTERPRISE'];
|
||||
};
|
||||
|
||||
const submitQuestion = async () => {
|
||||
const question = queryText.value.trim();
|
||||
if (!question || asking.value) return;
|
||||
asking.value = true;
|
||||
message.value = '';
|
||||
try {
|
||||
answer.value = await askPersonalAssistant({ queryText: question, scope: scopes.value, outputFormat: 'ACTION_PLAN' });
|
||||
await resolveCitationTypes(answer.value.citations || []);
|
||||
} catch (error) {
|
||||
const text = error instanceof Error ? error.message : '提问失败,请稍后重试';
|
||||
if (enterpriseEnabled.value && /ENTERPRISE_SCOPE_FORBIDDEN|企业知识|FORBIDDEN/i.test(text)) {
|
||||
scopes.value = ['PERSONAL'];
|
||||
message.value = '当前岗位没有企业知识权限,已仅查询我的资料。';
|
||||
try {
|
||||
answer.value = await askPersonalAssistant({ queryText: question, scope: ['PERSONAL'], outputFormat: 'ACTION_PLAN' });
|
||||
await resolveCitationTypes(answer.value.citations || []);
|
||||
} catch (personalError) {
|
||||
message.value = personalError instanceof Error ? personalError.message : '我的资料查询失败,请稍后重试';
|
||||
}
|
||||
} else {
|
||||
message.value = text;
|
||||
}
|
||||
} finally {
|
||||
asking.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const resolveCitationTypes = async (citations: PersonalCitation[]) => {
|
||||
const personalIds = Array.from(new Set(citations
|
||||
.filter((citation) => citation.domain === 'PERSONAL' && /^\d+$/.test(citation.sourceId))
|
||||
.map((citation) => citation.sourceId)));
|
||||
const resolved = await Promise.all(personalIds.map(async (id) => {
|
||||
try {
|
||||
const item = await loadPersonalItem(Number(id));
|
||||
return [id, item.sourceType] as const;
|
||||
} catch {
|
||||
return [id, ''] as const;
|
||||
}
|
||||
}));
|
||||
citationSourceTypes.value = Object.fromEntries(resolved);
|
||||
};
|
||||
|
||||
const citationLabel = (citation: PersonalCitation) => {
|
||||
if (citation.domain === 'ENTERPRISE') return '企业 SOP';
|
||||
return /^https?:/i.test(citation.sourceId) || citationSourceTypes.value[citation.sourceId] === 'URL' ? '外部网页' : '我的资料';
|
||||
};
|
||||
|
||||
const openCitation = (citation: PersonalCitation) => {
|
||||
if (citation.domain !== 'PERSONAL' || !/^\d+$/.test(citation.sourceId)) return;
|
||||
uni.navigateTo({ url: `/pages/user/assistant/item?id=${encodeURIComponent(citation.sourceId)}` });
|
||||
};
|
||||
|
||||
const goBack = () => uni.navigateBack();
|
||||
const openLibrary = () => uni.navigateTo({ url: '/pages/user/assistant/library' });
|
||||
const openCapture = () => uni.navigateTo({ url: '/pages/user/assistant/capture' });
|
||||
const openSessions = () => uni.navigateTo({ url: '/pages/user/assistant/sessions' });
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.assistant-page { overflow-x: hidden; }
|
||||
.domain-options, .shortcut-grid { display: grid; gap: 10px; }
|
||||
.domain-options { margin-top: 14px; grid-template-columns: 1fr 1fr; }
|
||||
.domain-option { min-width: 0; padding: 14px; border: 1px solid var(--line); background: var(--surface); text-align: left; }
|
||||
.domain-option.active { border-color: var(--brand); background: #fff6f7; }
|
||||
.domain-name, .domain-desc, .shortcut-title, .shortcut-desc, .answer-text, .citation-title, .citation-excerpt, .scope-note { display: block; }
|
||||
.domain-name, .shortcut-title { color: var(--ink); font-weight: 900; }
|
||||
.domain-desc, .shortcut-desc, .scope-note { margin-top: 5px; color: var(--muted); font-size: 12px; line-height: 1.45; }
|
||||
.ask-button { width: 100%; margin-top: 12px; }
|
||||
.shortcut-grid { margin-top: 16px; grid-template-columns: 1fr 1fr; }
|
||||
.shortcut { min-width: 0; padding: 15px; text-align: left; }
|
||||
.shortcut:last-child { grid-column: 1 / -1; }
|
||||
.answer-text { margin-top: 12px; line-height: 1.75; white-space: pre-wrap; }
|
||||
.citation-heading { display: block; margin: 18px 0 8px; font-weight: 900; }
|
||||
.citation-card { width: 100%; min-width: 0; margin-top: 8px; padding: 12px; border: 1px solid var(--line); background: #f8fafc; text-align: left; }
|
||||
.citation-head { display: flex; min-width: 0; gap: 8px; align-items: center; }
|
||||
.domain-badge { flex: 0 0 auto; padding: 3px 7px; border-radius: var(--radius); background: #eef3f9; color: var(--knowledge); font-size: 11px; }
|
||||
.domain-badge.enterprise { background: #fff2e6; color: var(--warning); }
|
||||
.citation-title { min-width: 0; overflow: hidden; font-weight: 800; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.citation-excerpt { margin-top: 7px; color: var(--muted); font-size: 13px; line-height: 1.55; }
|
||||
</style>
|
||||
Reference in New Issue
Block a user