feat: 完善人工关注与内容合规配置
This commit is contained in:
@@ -9,11 +9,14 @@ import MessageList, { type DisplayMessage } from "./components/MessageList.vue";
|
||||
import PersonalCenterDialog from "./components/PersonalCenterDialog.vue";
|
||||
import SessionDrawer from "./components/SessionDrawer.vue";
|
||||
import SessionQuota from "./components/SessionQuota.vue";
|
||||
import SiteFilingFooter from "./components/SiteFilingFooter.vue";
|
||||
import { ApiError, api, clearToken, getToken, saveToken, streamChat } from "./services/api";
|
||||
import { clearBehaviorEvents, flushBehaviorEvents, trackBehavior, type BehaviorEventCode } from "./services/behaviorTracker";
|
||||
import type { ChatMessage as ApiMessage, ChatSession, PeriodicReport, PracticeReviewResult, ShareDraft, TeacherHelpCard, UserProfile } from "./types/api";
|
||||
import type { ChatMessage as ApiMessage, ChatSession, PeriodicReport, PracticeReviewResult, PublicSiteConfig, ShareDraft, TeacherHelpCard, UserProfile } from "./types/api";
|
||||
import { withAiGeneratedNotice } from "./utils/aiGenerated";
|
||||
|
||||
const user = ref<UserProfile | null>(null);
|
||||
const publicSiteConfig = ref<PublicSiteConfig>({ filingText: "", filingUrl: "" });
|
||||
const sessions = ref<ChatSession[]>([]);
|
||||
const activeSessionId = ref<number | null>(null);
|
||||
const composerKey = ref(0);
|
||||
@@ -56,7 +59,18 @@ let reportPollStartedAt = 0;
|
||||
let reportRefreshPending = false;
|
||||
let trackedPersonalCenterSection: typeof personalCenterSection.value | null = null;
|
||||
|
||||
onMounted(bootstrap);
|
||||
onMounted(() => {
|
||||
void loadPublicSiteConfig();
|
||||
void bootstrap();
|
||||
});
|
||||
|
||||
async function loadPublicSiteConfig() {
|
||||
try {
|
||||
publicSiteConfig.value = await api.publicSiteConfig();
|
||||
} catch {
|
||||
publicSiteConfig.value = { filingText: "", filingUrl: "" };
|
||||
}
|
||||
}
|
||||
|
||||
async function bootstrap() {
|
||||
const search = new URLSearchParams(window.location.search);
|
||||
@@ -345,7 +359,7 @@ async function copyHelpCard() {
|
||||
}
|
||||
try {
|
||||
if (helpCard.value) trackBehavior("help_card_copy_click", { type: "help_card", id: helpCard.value.id });
|
||||
await copyText(helpCardContent.value);
|
||||
await copyText(withAiGeneratedNotice(helpCardContent.value));
|
||||
if (helpCard.value) {
|
||||
helpCard.value = await api.markHelpCardCopied(helpCard.value.id);
|
||||
}
|
||||
@@ -381,7 +395,7 @@ async function copyShareDraft() {
|
||||
}
|
||||
try {
|
||||
if (shareDraft.value) trackBehavior("share_draft_copy_click", { type: "share_draft", id: shareDraft.value.id });
|
||||
await copyText(shareDraftContent.value);
|
||||
await copyText(withAiGeneratedNotice(shareDraftContent.value));
|
||||
if (shareDraft.value) {
|
||||
shareDraft.value = await api.markShareDraftCopied(shareDraft.value.id);
|
||||
}
|
||||
@@ -394,7 +408,7 @@ async function copyShareDraft() {
|
||||
async function copyHistoryHelpCard(card: TeacherHelpCard) {
|
||||
try {
|
||||
trackBehavior("help_card_copy_click", { type: "help_card", id: card.id });
|
||||
await copyText(card.content);
|
||||
await copyText(withAiGeneratedNotice(card.content));
|
||||
const updated = await api.markHelpCardCopied(card.id);
|
||||
helpCardHistory.value = helpCardHistory.value.map((item) => item.id === updated.id ? updated : item);
|
||||
showToast("求助卡已复制,可再次粘贴给老师");
|
||||
@@ -406,7 +420,7 @@ async function copyHistoryHelpCard(card: TeacherHelpCard) {
|
||||
async function copyHistoryShareDraft(draft: ShareDraft) {
|
||||
try {
|
||||
trackBehavior("share_draft_copy_click", { type: "share_draft", id: draft.id });
|
||||
await copyText(draft.content);
|
||||
await copyText(withAiGeneratedNotice(draft.content));
|
||||
const updated = await api.markShareDraftCopied(draft.id);
|
||||
shareDraftHistory.value = shareDraftHistory.value.map((item) => item.id === updated.id ? updated : item);
|
||||
showToast("分享稿已复制,可再次粘贴到班级群");
|
||||
@@ -688,9 +702,9 @@ async function copyText(text: string) {
|
||||
|
||||
<template>
|
||||
<main class="app-shell">
|
||||
<section class="phone-frame" :class="{ 'login-mode': !user && !booting, 'chat-mode': Boolean(user) }">
|
||||
<section class="phone-frame" :class="{ 'login-mode': !user && !booting, 'chat-mode': Boolean(user), 'has-filing': Boolean(publicSiteConfig.filingText) }">
|
||||
<div v-if="booting" class="booting">正在启动...</div>
|
||||
<LoginPanel v-else-if="!user" @logged-in="onLoggedIn" />
|
||||
<LoginPanel v-else-if="!user" :filing-text="publicSiteConfig.filingText" :filing-url="publicSiteConfig.filingUrl" @logged-in="onLoggedIn" />
|
||||
<template v-else>
|
||||
<ChatHeader
|
||||
:user="user"
|
||||
@@ -716,6 +730,7 @@ async function copyText(text: string) {
|
||||
@feedback="openFeedback"
|
||||
/>
|
||||
<ChatComposer :key="composerKey" :loading="sending" :disabled="!activeSessionId || loadingSession" @send="send" @stop="stop" @behavior="trackBehavior" />
|
||||
<SiteFilingFooter :text="publicSiteConfig.filingText" :url="publicSiteConfig.filingUrl" />
|
||||
<SessionDrawer
|
||||
:open="drawerOpen"
|
||||
:sessions="sessions"
|
||||
@@ -763,6 +778,7 @@ async function copyText(text: string) {
|
||||
|
||||
<AppDialog v-if="helpCardDialogOpen" title="老师求助卡" labelled-by="help-card-title" @close="helpCardDialogOpen = false">
|
||||
<section class="help-card-dialog">
|
||||
<span class="ai-generated-badge">AI生成</span>
|
||||
<p>这不是转人工工单,系统不会自动发送给老师。你可以按真实情况删改后,自行复制给老师或班级群确认。</p>
|
||||
<textarea v-model="helpCardContent" aria-label="求助卡内容" />
|
||||
</section>
|
||||
@@ -776,6 +792,7 @@ async function copyText(text: string) {
|
||||
|
||||
<AppDialog v-if="shareDraftDialogOpen" title="班级分享稿" labelled-by="share-draft-title" @close="shareDraftDialogOpen = false">
|
||||
<section class="help-card-dialog">
|
||||
<span class="ai-generated-badge">AI生成</span>
|
||||
<p>这只是分享草稿,系统不会自动发送到任何群。请删掉不想公开的隐私内容,并按自己的真实状态修改后再复制。</p>
|
||||
<textarea v-model="shareDraftContent" aria-label="班级分享稿内容" />
|
||||
</section>
|
||||
|
||||
@@ -111,6 +111,7 @@ const displayTime = computed(() => {
|
||||
</div>
|
||||
</template>
|
||||
<div v-else class="message-content">{{ renderedContent }}</div>
|
||||
<span v-if="role === 'assistant' && renderedContent" class="ai-generated-badge">AI生成</span>
|
||||
<time v-if="displayTime" :datetime="createdAt">{{ displayTime }}</time>
|
||||
<button v-if="role === 'assistant' && !streaming && !errorMessage && /^\d+$/.test(messageId)" type="button" class="message-feedback-button" @click="emit('feedback')">
|
||||
<MessageSquareWarning :size="14" aria-hidden="true" />
|
||||
|
||||
@@ -14,6 +14,12 @@ import type { UserProfile } from "../types/api";
|
||||
import AppDialog from "./AppDialog.vue";
|
||||
import HelpDialog from "./HelpDialog.vue";
|
||||
import PolicyDialog from "./PolicyDialog.vue";
|
||||
import SiteFilingFooter from "./SiteFilingFooter.vue";
|
||||
|
||||
defineProps<{
|
||||
filingText?: string;
|
||||
filingUrl?: string;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
loggedIn: [user: UserProfile];
|
||||
@@ -324,6 +330,7 @@ async function keepFieldVisible(event: FocusEvent) {
|
||||
<span></span><ShieldCheck :size="20" aria-hidden="true" /><span></span>
|
||||
<p>仅用于身份验证与账号登录,不会泄露你的信息</p>
|
||||
</footer>
|
||||
<SiteFilingFooter :text="filingText || ''" :url="filingUrl" />
|
||||
</div>
|
||||
|
||||
<div v-if="toast" class="login-toast" role="status" aria-live="polite">{{ toast }}</div>
|
||||
|
||||
@@ -226,6 +226,7 @@ function submitDeleteCard() {
|
||||
<div class="section-heading">
|
||||
<Sparkles :size="18" aria-hidden="true" />
|
||||
<strong>近期实修回顾</strong>
|
||||
<span class="ai-generated-badge">AI生成</span>
|
||||
</div>
|
||||
<p>{{ practiceReview.profile.reviewText }}</p>
|
||||
<div v-if="practiceReview.profile.currentFocus" class="current-focus">
|
||||
@@ -289,7 +290,7 @@ function submitDeleteCard() {
|
||||
<summary>
|
||||
<CalendarDays :size="16" aria-hidden="true" />
|
||||
<span>{{ item.title }}</span>
|
||||
<small>{{ item.reportTypeLabel }}</small>
|
||||
<small>{{ item.reportTypeLabel }} · AI生成</small>
|
||||
</summary>
|
||||
<time>{{ formatDate(item.periodStart) }} 至 {{ formatDate(item.periodEnd) }}</time>
|
||||
<pre>{{ item.content }}</pre>
|
||||
@@ -343,7 +344,7 @@ function submitDeleteCard() {
|
||||
<summary>
|
||||
<div>
|
||||
<time>{{ formatDate(item.createdAt) }}</time>
|
||||
<strong>{{ item.copied ? "已复制给老师" : "尚未复制" }}</strong>
|
||||
<strong>{{ item.copied ? "已复制给老师" : "尚未复制" }} · AI生成</strong>
|
||||
<p>{{ item.content.slice(0, 72) }}{{ item.content.length > 72 ? "…" : "" }}</p>
|
||||
</div>
|
||||
<ChevronDown :size="17" aria-hidden="true" />
|
||||
@@ -382,7 +383,7 @@ function submitDeleteCard() {
|
||||
<summary>
|
||||
<div>
|
||||
<time>{{ formatDate(item.createdAt) }}</time>
|
||||
<strong>{{ item.copied ? "已复制" : "尚未复制" }}</strong>
|
||||
<strong>{{ item.copied ? "已复制" : "尚未复制" }} · AI生成</strong>
|
||||
<p>{{ item.content.slice(0, 72) }}{{ item.content.length > 72 ? "…" : "" }}</p>
|
||||
</div>
|
||||
<ChevronDown :size="17" aria-hidden="true" />
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
text: string;
|
||||
url?: string;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<footer v-if="text" class="site-filing-footer" aria-label="网站备案信息">
|
||||
<a v-if="url" :href="url" target="_blank" rel="noopener noreferrer nofollow">{{ text }}</a>
|
||||
<span v-else>{{ text }}</span>
|
||||
</footer>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.site-filing-footer {
|
||||
flex: 0 0 auto;
|
||||
padding: 6px 14px max(6px, env(safe-area-inset-bottom));
|
||||
border-top: 1px solid rgba(121, 143, 134, 0.14);
|
||||
background: rgba(248, 251, 250, 0.96);
|
||||
color: #82918b;
|
||||
font-size: 11px;
|
||||
line-height: 1.45;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.site-filing-footer a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.site-filing-footer a:hover,
|
||||
.site-filing-footer a:focus-visible {
|
||||
color: #0f705d;
|
||||
text-decoration: underline;
|
||||
}
|
||||
</style>
|
||||
@@ -6,6 +6,7 @@ import type {
|
||||
PracticeReviewResult,
|
||||
LoginResult,
|
||||
PeriodicReport,
|
||||
PublicSiteConfig,
|
||||
ShareDraft,
|
||||
TeacherHelpCard,
|
||||
UserProfile,
|
||||
@@ -78,6 +79,7 @@ async function request<T>(path: string, init: RequestInit = {}, options: Request
|
||||
}
|
||||
|
||||
export const api = {
|
||||
publicSiteConfig: () => request<PublicSiteConfig>("/site/config"),
|
||||
captcha: () => request<CaptchaResult>("/auth/captcha"),
|
||||
sendSms: (phone: string, captchaId?: string, captchaCode?: string) =>
|
||||
request<null>("/auth/sms/send", {
|
||||
|
||||
@@ -1126,6 +1126,14 @@ textarea:focus-visible {
|
||||
background: var(--chat-bg);
|
||||
}
|
||||
|
||||
.phone-frame.chat-mode.has-filing {
|
||||
grid-template-rows: auto auto minmax(0, 1fr) auto auto;
|
||||
}
|
||||
|
||||
.phone-frame.chat-mode.has-filing .chat-composer {
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.chat-header {
|
||||
position: relative;
|
||||
z-index: 5;
|
||||
@@ -1603,6 +1611,7 @@ textarea:focus-visible {
|
||||
|
||||
.message-feedback-button { margin-top: 8px; display: inline-flex; align-items: center; gap: 5px; padding: 4px 8px; border: 0; border-radius: 7px; background: transparent; color: var(--chat-weak); font-size: 12px; }
|
||||
.message-feedback-button:hover { background: rgba(20, 148, 119, 0.08); color: var(--chat-brand-deep); }
|
||||
.ai-generated-badge { display: inline-flex; width: fit-content; align-items: center; justify-content: center; margin-top: 7px; padding: 2px 6px; border: 1px solid rgba(31, 118, 93, 0.18); border-radius: 999px; background: #f1f8f5; color: #4f7568; font-size: 10px; font-weight: 700; line-height: 1.4; }
|
||||
.feedback-dialog p { margin: 0 0 12px; color: var(--chat-muted); line-height: 1.65; }
|
||||
.feedback-dialog textarea { width: 100%; resize: vertical; padding: 12px; border: 1px solid var(--chat-control-border); border-radius: 10px; font: inherit; }
|
||||
.feedback-dialog-footer { display: flex; align-items: center; justify-content: space-between; margin-top: 10px; color: var(--chat-weak); font-size: 12px; }
|
||||
|
||||
@@ -4,6 +4,11 @@ export interface ApiResponse<T> {
|
||||
data: T;
|
||||
}
|
||||
|
||||
export interface PublicSiteConfig {
|
||||
filingText: string;
|
||||
filingUrl: string;
|
||||
}
|
||||
|
||||
export interface UserProfile {
|
||||
id: number;
|
||||
phone: string;
|
||||
@@ -75,6 +80,7 @@ export interface PeriodicReport {
|
||||
periodEnd: string;
|
||||
title: string;
|
||||
content: string;
|
||||
aiGenerated: boolean;
|
||||
status: "pending" | "running" | "success" | "failed" | "empty" | string;
|
||||
nextRunAt?: string | null;
|
||||
finishedAt?: string | null;
|
||||
@@ -87,6 +93,7 @@ export interface TeacherHelpCard {
|
||||
topicSessionId: number;
|
||||
summaryId?: number | null;
|
||||
content: string;
|
||||
aiGenerated: boolean;
|
||||
source: string;
|
||||
copied: boolean;
|
||||
copiedAt?: string | null;
|
||||
@@ -99,6 +106,7 @@ export interface ShareDraft {
|
||||
topicSessionId: number;
|
||||
summaryId?: number | null;
|
||||
content: string;
|
||||
aiGenerated: boolean;
|
||||
source: string;
|
||||
copied: boolean;
|
||||
copiedAt?: string | null;
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
export const AI_GENERATED_NOTICE = "AI生成内容,请结合实际情况核对后使用。";
|
||||
|
||||
export function withAiGeneratedNotice(content: string) {
|
||||
const text = content.trim();
|
||||
if (!text || text.includes(AI_GENERATED_NOTICE)) return text;
|
||||
return `${text}\n\n—— ${AI_GENERATED_NOTICE}`;
|
||||
}
|
||||
Reference in New Issue
Block a user