feat: add topic summaries and growth profiles

This commit is contained in:
2026-07-31 15:34:03 +08:00
parent 884dc765ad
commit 3e60a6da42
22 changed files with 1040 additions and 5 deletions

View File

@@ -9,7 +9,7 @@ import MessageList, { type DisplayMessage } from "./components/MessageList.vue";
import SessionDrawer from "./components/SessionDrawer.vue";
import SessionQuota from "./components/SessionQuota.vue";
import { ApiError, api, clearToken, getToken, streamChat } from "./services/api";
import type { ChatMessage as ApiMessage, ChatSession, UserProfile } from "./types/api";
import type { ChatMessage as ApiMessage, ChatSession, GrowthProfileResult, UserProfile } from "./types/api";
const user = ref<UserProfile | null>(null);
const sessions = ref<ChatSession[]>([]);
@@ -22,6 +22,10 @@ const loadingSession = ref(false);
const loadingSessions = ref(false);
const sessionOperationPending = ref(false);
const logoutDialogOpen = ref(false);
const profileDialogOpen = ref(false);
const growthProfile = ref<GrowthProfileResult | null>(null);
const profileLoading = ref(false);
const finishingTopic = ref(false);
const statusText = ref("连接后端中");
const toastText = ref("");
const followingOutput = ref(true);
@@ -195,6 +199,33 @@ async function stop() {
}
}
async function finishCurrentTopic() {
if (!activeSessionId.value || sending.value || finishingTopic.value) return;
finishingTopic.value = true;
try {
const result = await api.finishTopic(activeSessionId.value);
showToast(result.growthProfileEnabled ? "本主题已沉淀,并更新了你的成长档案" : "本主题已沉淀");
await refreshSessionList();
await refreshProfile();
} catch (error) {
handleError(error, "主题沉淀失败");
} finally {
finishingTopic.value = false;
}
}
async function openGrowthProfile() {
profileDialogOpen.value = true;
profileLoading.value = true;
try {
growthProfile.value = await api.growthProfile();
} catch (error) {
handleError(error, "成长档案加载失败");
} finally {
profileLoading.value = false;
}
}
async function renameSession(sessionId: number, title: string, done: (success: boolean) => void) {
if (sessionOperationPending.value) return;
sessionOperationPending.value = true;
@@ -304,7 +335,14 @@ function showToast(message: string) {
<LoginPanel v-else-if="!user" @logged-in="onLoggedIn" />
<template v-else>
<ChatHeader :user="user" :status-text="statusText" @open-history="drawerOpen = true" @logout="logoutDialogOpen = true" />
<SessionQuota :used="user.todayUsed" :limit="user.dailyLimit" :entitlement="user.entitlement" />
<SessionQuota
:used="user.todayUsed"
:limit="user.dailyLimit"
:entitlement="user.entitlement"
:finishing="finishingTopic"
@finish-topic="finishCurrentTopic"
@open-profile="openGrowthProfile"
/>
<MessageList
ref="messageList"
:messages="messages"
@@ -329,6 +367,30 @@ function showToast(message: string) {
<div v-if="toastText" class="chat-toast" role="status">{{ toastText }}</div>
<AppDialog v-if="profileDialogOpen" title="我的实修档案" labelled-by="growth-profile-title" @close="profileDialogOpen = false">
<section class="growth-profile-dialog" :aria-busy="profileLoading">
<p v-if="profileLoading" class="profile-empty">正在加载成长档案...</p>
<template v-else-if="growthProfile?.profile">
<pre>{{ growthProfile.profile.profileText }}</pre>
<div class="growth-profile-fields">
<p v-if="growthProfile.profile.recurringTopics"><strong>反复议题</strong>{{ growthProfile.profile.recurringTopics }}</p>
<p v-if="growthProfile.profile.commonEmotions"><strong>常见情绪</strong>{{ growthProfile.profile.commonEmotions }}</p>
<p v-if="growthProfile.profile.bodyPatterns"><strong>身体感受</strong>{{ growthProfile.profile.bodyPatterns }}</p>
<p v-if="growthProfile.profile.homeworkDone"><strong>做过的功课</strong>{{ growthProfile.profile.homeworkDone }}</p>
<p v-if="growthProfile.profile.recentProgress"><strong>最近进展</strong>{{ growthProfile.profile.recentProgress }}</p>
</div>
</template>
<p v-else class="profile-empty">还没有成长档案你可以在完成一次主题对话后点击沉淀本主题</p>
<div v-if="growthProfile?.recentSummaries.length" class="recent-topic-summaries">
<h3>最近主题沉淀</h3>
<article v-for="item in growthProfile.recentSummaries" :key="item.id">
<time>{{ item.generatedAt }}</time>
<p>{{ item.summary }}</p>
</article>
</div>
</section>
</AppDialog>
<AppDialog v-if="logoutDialogOpen" title="退出登录" labelled-by="logout-dialog-title" @close="logoutDialogOpen = false">
<p class="confirm-copy">确定退出当前账号吗</p>
<template #footer>