feat(personal): add editable report outlines
This commit is contained in:
@@ -60,14 +60,37 @@
|
||||
</button>
|
||||
</view>
|
||||
<view v-else class="empty">本次回答没有可展示的引用,不补造来源。</view>
|
||||
<button v-if="answer.citations?.length" class="secondary-button outline-create" :disabled="outlineBusy" @click="generateOutline">
|
||||
{{ outlineBusy ? '正在生成提纲' : '生成汇报提纲' }}
|
||||
</button>
|
||||
</view>
|
||||
|
||||
<view v-if="outline" class="panel employee-card outline-panel">
|
||||
<view class="outline-head"><text class="panel-title">汇报提纲</text><text class="outline-state">{{ outline.confirmed ? '已确认' : '可编辑' }}</text></view>
|
||||
<input v-model="outline.title" class="outline-input outline-title" :disabled="outline.confirmed" maxlength="120" placeholder="汇报标题" />
|
||||
<view v-for="(section, sectionIndex) in outline.sections" :key="sectionIndex" class="outline-section">
|
||||
<input v-model="section.title" class="outline-input section-title" :disabled="outline.confirmed" maxlength="80" placeholder="章节标题" />
|
||||
<input v-for="(_bullet, bulletIndex) in section.bullets" :key="bulletIndex" v-model="section.bullets[bulletIndex]" class="outline-input bullet-input" :disabled="outline.confirmed" maxlength="240" placeholder="要点" />
|
||||
<button v-if="!outline.confirmed" class="text-button" @click="addOutlineBullet(sectionIndex)">+ 添加要点</button>
|
||||
</view>
|
||||
<button v-if="!outline.confirmed" class="text-button" @click="addOutlineSection">+ 添加章节</button>
|
||||
<view class="outline-sources">
|
||||
<text class="citation-heading">资料来源不可编辑</text>
|
||||
<text v-for="source in outline.sources" :key="`${source.domain}-${source.sourceId}`" class="outline-source">{{ source.domain === 'ENTERPRISE' ? '企业知识' : '我的资料' }} · {{ source.title }}</text>
|
||||
</view>
|
||||
<view v-if="!outline.confirmed" class="outline-actions">
|
||||
<button class="secondary-button" :disabled="outlineBusy" @click="saveOutline">保存修改</button>
|
||||
<button class="primary-button" :disabled="outlineBusy" @click="confirmOutline">确认大纲</button>
|
||||
</view>
|
||||
<text v-if="outlineMessage" class="status-message">{{ outlineMessage }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue';
|
||||
import type { PersonalAskResponse, PersonalCitation } from '@/types/api';
|
||||
import { askPersonalAssistant, type KnowledgeScope } from '@/services/personal-assistant';
|
||||
import type { PersonalAskResponse, PersonalCitation, PersonalExportOutline } from '@/types/api';
|
||||
import { askPersonalAssistant, confirmPersonalOutline, createPersonalOutline, updatePersonalOutline, type KnowledgeScope } from '@/services/personal-assistant';
|
||||
import { personalItemRoute } from '@/services/personal-assistant-id';
|
||||
|
||||
const scopes = ref<KnowledgeScope[]>(['PERSONAL']);
|
||||
@@ -75,6 +98,9 @@ const queryText = ref('');
|
||||
const asking = ref(false);
|
||||
const answer = ref<PersonalAskResponse>();
|
||||
const message = ref('');
|
||||
const outline = ref<PersonalExportOutline>();
|
||||
const outlineBusy = ref(false);
|
||||
const outlineMessage = ref('');
|
||||
let requestGeneration = 0;
|
||||
const enterpriseEnabled = computed(() => scopes.value.includes('ENTERPRISE'));
|
||||
|
||||
@@ -83,6 +109,7 @@ const changeScope = () => {
|
||||
if (asking.value) return;
|
||||
requestGeneration += 1;
|
||||
answer.value = undefined;
|
||||
outline.value = undefined;
|
||||
message.value = '';
|
||||
scopes.value = enterpriseEnabled.value ? ['PERSONAL'] : ['PERSONAL', 'ENTERPRISE'];
|
||||
};
|
||||
@@ -94,6 +121,7 @@ const submitQuestion = async () => {
|
||||
const scopeSnapshot = [...scopes.value];
|
||||
asking.value = true;
|
||||
answer.value = undefined;
|
||||
outline.value = undefined;
|
||||
message.value = '';
|
||||
try {
|
||||
const response = await askPersonalAssistant({ queryText: question, scope: scopeSnapshot, outputFormat: 'ACTION_PLAN' });
|
||||
@@ -123,6 +151,64 @@ const submitQuestion = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
const generateOutline = async () => {
|
||||
if (!answer.value?.sessionId || !answer.value.citations?.length || outlineBusy.value) return;
|
||||
outlineBusy.value = true;
|
||||
outlineMessage.value = '';
|
||||
try {
|
||||
outline.value = await createPersonalOutline({ sessionId: answer.value.sessionId, title: queryText.value.trim() });
|
||||
} catch (error) {
|
||||
outlineMessage.value = error instanceof Error ? error.message : '提纲生成失败';
|
||||
} finally {
|
||||
outlineBusy.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const outlinePayload = () => outline.value ? {
|
||||
title: outline.value.title.trim(),
|
||||
sections: outline.value.sections.map((section) => ({
|
||||
title: section.title.trim(),
|
||||
bullets: section.bullets.map((bullet) => bullet.trim()).filter(Boolean)
|
||||
})).filter((section) => section.title && section.bullets.length)
|
||||
} : undefined;
|
||||
|
||||
const saveOutline = async () => {
|
||||
const payload = outlinePayload();
|
||||
if (!outline.value || !payload || !payload.title || !payload.sections.length || outlineBusy.value) {
|
||||
outlineMessage.value = '请保留标题和至少一个有效章节';
|
||||
return;
|
||||
}
|
||||
outlineBusy.value = true;
|
||||
outlineMessage.value = '';
|
||||
try {
|
||||
outline.value = await updatePersonalOutline(outline.value.id, payload);
|
||||
outlineMessage.value = '提纲已保存';
|
||||
} catch (error) {
|
||||
outlineMessage.value = error instanceof Error ? error.message : '提纲保存失败';
|
||||
} finally {
|
||||
outlineBusy.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const confirmOutline = async () => {
|
||||
await saveOutline();
|
||||
if (!outline.value || outlineMessage.value !== '提纲已保存') return;
|
||||
outlineBusy.value = true;
|
||||
try {
|
||||
await confirmPersonalOutline(outline.value.id);
|
||||
outline.value.confirmed = true;
|
||||
outline.value.status = 'CONFIRMED';
|
||||
outlineMessage.value = '大纲已确认,内容已冻结';
|
||||
} catch (error) {
|
||||
outlineMessage.value = error instanceof Error ? error.message : '大纲确认失败';
|
||||
} finally {
|
||||
outlineBusy.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const addOutlineSection = () => outline.value?.sections.push({ title: '', bullets: [''] });
|
||||
const addOutlineBullet = (sectionIndex: number) => outline.value?.sections[sectionIndex]?.bullets.push('');
|
||||
|
||||
const citationLabel = (citation: PersonalCitation) => {
|
||||
if (citation.domain === 'ENTERPRISE') return '企业 SOP';
|
||||
return citation.sourceType === 'URL' ? '外部网页' : '我的资料';
|
||||
@@ -160,4 +246,5 @@ const openSessions = () => uni.navigateTo({ url: '/pages/user/assistant/sessions
|
||||
.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; }
|
||||
.outline-create { width: 100%; margin-top: 14px; }.outline-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; }.outline-state { color: var(--brand); font-size: 12px; font-weight: 800; }.outline-input { width: 100%; min-height: 42px; margin-top: 10px; padding: 0 12px; border: 1px solid var(--line); background: #fff; color: var(--ink); }.outline-title { font-size: 17px; font-weight: 900; }.outline-section { margin-top: 14px; padding: 12px; border: 1px solid var(--line); background: #f8fafc; }.section-title { margin-top: 0; font-weight: 800; }.bullet-input { font-size: 13px; }.text-button { margin-top: 8px; padding: 4px 0; border: 0; background: transparent; color: var(--brand); font-size: 13px; }.outline-sources { margin-top: 14px; }.outline-source { display: block; margin-top: 6px; color: var(--muted); font-size: 12px; }.outline-actions { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-top: 16px; }.outline-actions button { width: 100%; }
|
||||
</style>
|
||||
|
||||
@@ -3,6 +3,8 @@ import type {
|
||||
PersonalItem,
|
||||
PersonalItemCreated,
|
||||
PersonalItemSourceType,
|
||||
PersonalExportOutline,
|
||||
PersonalExportOutlineSection,
|
||||
PersonalKnowledgeScope,
|
||||
PersonalPage,
|
||||
PersonalSession,
|
||||
@@ -164,3 +166,24 @@ export const deletePersonalSession = (id: string) => apiRequest<void>({
|
||||
url: personalSessionApiPath(id),
|
||||
method: 'DELETE'
|
||||
});
|
||||
|
||||
export const createPersonalOutline = (payload: { sessionId: string; title?: string }) =>
|
||||
apiRequest<PersonalExportOutline>({
|
||||
url: '/api/aihr/personal-assistant/exports/outline',
|
||||
method: 'POST',
|
||||
data: payload
|
||||
});
|
||||
|
||||
export const updatePersonalOutline = (id: string, payload: {
|
||||
title: string;
|
||||
sections: PersonalExportOutlineSection[];
|
||||
}) => apiRequest<PersonalExportOutline>({
|
||||
url: `/api/aihr/personal-assistant/exports/${encodeURIComponent(id)}/outline`,
|
||||
method: 'PUT',
|
||||
data: payload
|
||||
});
|
||||
|
||||
export const confirmPersonalOutline = (id: string) => apiRequest<void>({
|
||||
url: `/api/aihr/personal-assistant/exports/${encodeURIComponent(id)}/confirm`,
|
||||
method: 'POST'
|
||||
});
|
||||
|
||||
@@ -139,6 +139,30 @@ export interface PersonalAskResponse {
|
||||
promptVersion?: string;
|
||||
}
|
||||
|
||||
export interface PersonalExportOutlineSection {
|
||||
title: string;
|
||||
bullets: string[];
|
||||
}
|
||||
|
||||
export interface PersonalExportOutlineSource {
|
||||
domain: PersonalKnowledgeScope;
|
||||
sourceId: string;
|
||||
itemId?: string;
|
||||
title: string;
|
||||
excerpt: string;
|
||||
}
|
||||
|
||||
export interface PersonalExportOutline {
|
||||
id: string;
|
||||
sessionId: string;
|
||||
title: string;
|
||||
sections: PersonalExportOutlineSection[];
|
||||
sources: PersonalExportOutlineSource[];
|
||||
confirmed: boolean;
|
||||
status: string;
|
||||
ossId?: string;
|
||||
}
|
||||
|
||||
export interface PersonalSpace {
|
||||
spaceId: string;
|
||||
status: string;
|
||||
|
||||
Reference in New Issue
Block a user