refactor(ai): 稿件与大纲页文案去掉接口术语与多余提示

面向学生的页面不再暴露实现细节:

- 删除两页底部「提交说明」整块(含 `docType`/`inputs`、接口路径、
  WebSocket 流式说明及对应样式)
- 稿件页删除 `user_content`、`files` 字段说明;大纲页不再把
  conversationId/messageId 拼成「接口摘要」展示,随之删除
  buildSummary、resultSummary 及摘要渲染
- 加载态由三段式压成一行,空态与附件说明各留一句人话
This commit is contained in:
2026-10-23 12:23:32 +08:00
parent e210a9b4b7
commit 5073b2969a
2 changed files with 13 additions and 180 deletions
+5 -84
View File
@@ -4,11 +4,7 @@
<div>
<div class="hero-kicker">AI EVENT PLANNING OUTLINE</div>
<h1>活动策划大纲生成</h1>
<p>
输入活动主题、受众、时间和资金等信息,系统将调用
`/ai/activityOutlineGen`
生成结构化活动策划大纲,适合方案初稿、汇报材料和执行拆解场景。
</p>
<p>填写活动主题、受众、时间和预算,AI 给出一份结构完整的策划大纲,可直接改、复制或导出。</p>
</div>
</div>
@@ -91,13 +87,6 @@
</a-button>
</div>
<div class="hint-block">
<div class="hint-title">提交说明</div>
<div class="hint-text">
必填字段会直接作为 `inputs` 透传给
AI,后端固定触发“活动策划大纲生成”指令,并返回整理后的正文结果。
</div>
</div>
</a-card>
</a-col>
@@ -140,31 +129,19 @@
<a-empty
v-if="!loading && !hasOutlineContent"
description="填写左侧信息后开始生成活动策划大纲"
description="填写活动信息后开始生成"
/>
<div v-else class="result-shell">
<div v-if="loading" class="loading-panel">
<div class="loading-kicker">OUTLINE GENERATING</div>
<div class="loading-title">AI 正在整理活动策划结构</div>
<div class="loading-desc">
正在综合活动主题、受众、时间与经费信息,生成可直接二次编辑的策划大纲。
</div>
<div class="loading-title">正在生成活动策划大纲…</div>
</div>
<div v-if="showThinkingIndicator" class="outline-thinking">
<span class="thinking-dot" aria-hidden="true"></span>
<span>AI 正在生成正文内容,请稍候...</span>
<span>正在生成正文…</span>
</div>
<a-alert
v-if="resultSummary"
class="result-alert"
type="info"
show-icon
:message="resultSummary"
/>
<ByteMdEditor
v-if="hasOutlineContent"
v-model:value="editableOutlineMarkdown"
@@ -236,7 +213,6 @@
const result = ref<ActivityOutlineGenResponse | null>(null);
const outlineMarkdown = ref('');
const editableOutlineMarkdown = ref('');
const resultSummary = ref('');
const activeConversationId = ref<string>();
const activeMessageId = ref<string>();
const webSocketRef = ref<WebSocket | null>(null);
@@ -412,17 +388,6 @@
});
};
const buildSummary = () => {
const parts = [] as string[];
if (activeConversationId.value) {
parts.push(`conversationId: ${activeConversationId.value}`);
}
if (activeMessageId.value) {
parts.push(`messageId: ${activeMessageId.value}`);
}
return parts.join(' | ');
};
const clearStreamResolver = () => {
streamResolverRef.value = null;
};
@@ -485,7 +450,6 @@
outlineMarkdown.value += chunk;
}
syncEditableOutline();
resultSummary.value = buildSummary() || '正在通过 WebSocket 接收流式内容';
};
const finishStream = (conversationId?: string, messageId?: string) => {
@@ -495,7 +459,6 @@
if (messageId && messageId !== '0') {
activeMessageId.value = messageId;
}
resultSummary.value = buildSummary() || '已通过 WebSocket 接收生成结果';
loading.value = false;
resolveStream();
disconnectWebSocket();
@@ -512,7 +475,6 @@
return;
}
if (data?.loading === 'true' || data?.loading === true) {
resultSummary.value = 'AI 正在组织活动策划大纲结构...';
return;
}
const chunk = pickRawString(data?.notice, data?.answer);
@@ -608,7 +570,6 @@
result.value = null;
outlineMarkdown.value = '';
editableOutlineMarkdown.value = '';
resultSummary.value = '';
activeConversationId.value = undefined;
activeMessageId.value = undefined;
};
@@ -631,13 +592,11 @@
}
loading.value = true;
clearResult();
resultSummary.value = '正在提交生成任务...';
try {
// 一律走 HTTP 流式(/stream),不再先连 WebSocket:
// 触发生成的那个 POST 在整段生成期间是零字节的,会被链路上的 60s 空闲
// 超时掐成 504 Gateway Time-out。/stream 每 15s 有后端心跳续命。
const payload = buildPayload();
resultSummary.value = '已提交生成任务,等待流式返回...';
const waitStream = createStreamWaiter();
await streamActivityOutlineGen(
payload as unknown as Record<string, unknown>,
@@ -895,27 +854,6 @@
margin-top: 8px;
}
.hint-block {
margin-top: 18px;
padding: 16px 18px;
border-radius: 16px;
background: #f6f9fe;
border: 1px solid #d9e6fb;
}
.hint-title {
margin-bottom: 8px;
font-size: 13px;
font-weight: 600;
color: #274f84;
}
.hint-text {
color: #5f6f84;
line-height: 1.8;
font-size: 13px;
}
.result-card :deep(.ant-card-head) {
border-bottom: 1px solid #edf2f8;
}
@@ -944,29 +882,12 @@
border: 1px solid #d9e8fb;
}
.loading-kicker {
margin-bottom: 10px;
font-size: 12px;
letter-spacing: 0.18em;
color: #4875b0;
}
.loading-title {
font-size: 22px;
font-size: 18px;
font-weight: 600;
color: #183b68;
}
.loading-desc {
margin-top: 10px;
line-height: 1.8;
color: #607086;
}
.result-alert {
border-radius: 14px;
}
.outline-thinking {
display: inline-flex;
align-items: center;
+8 -96
View File
@@ -4,10 +4,7 @@
<div>
<div class="hero-kicker">AI CONTENT STUDIO</div>
<h1>AI内容创作与宣传助手</h1>
<p>
面向团委宣传、活动报道、新闻稿和推文草稿的快速生成场景。输入创作条件后,通过
`/ai/manuscriptGen` 发起生成任务,并通过 WebSocket 实时接收返回内容,支持继续编辑与复制。
</p>
<p>写新闻稿、推文、发言稿,或直接说明你的需求,AI 先给出一份初稿,可以接着改。</p>
</div>
<div class="hero-actions">
<a-button type="primary" ghost @click="goChatPage">进入问答助手</a-button>
@@ -41,9 +38,6 @@
:auto-size="{ minRows: 6, maxRows: 12 }"
placeholder="例如:帮我写一段迎新晚会的主持人开场词,口语化一点,时长两分钟左右"
/>
<div class="upload-tip">
不套模板时,这段内容会作为 `user_content` 交给「AI 创意助手」;选上面的文稿模板后可填写活动信息等可选参数。
</div>
</a-form-item>
<template v-if="templateMode">
<a-form-item label="稿件标题">
@@ -111,9 +105,6 @@
上传附件
</a-button>
</a-upload>
<div class="upload-tip">
支持图片、文档等附件,上传后按 `files` 字段一并提交给接口。
</div>
<div v-if="uploadedFiles.length" class="file-list">
<div
v-for="(item, index) in uploadedFiles"
@@ -145,15 +136,6 @@
生成内容
</a-button>
</div>
<div class="hint-block">
<div class="hint-title">提交说明</div>
<div class="hint-text">
未选模板(自由创作)时只提交一段内容,后端转「AI 创意助手」;选了模板才提交
`docType` 等结构化条件。两种都会先 `POST /ai/manuscriptGen`,再按 `/ai` 页面的方式
通过 WebSocket 流式接收生成结果;如果后端仍然直接返回正文,页面也会自动兼容解析。
</div>
</div>
</a-card>
</a-col>
@@ -176,7 +158,7 @@
</a-space>
</template>
<a-empty v-if="!markdownContent && !loading" description="填写左侧创作条件后开始生成" />
<a-empty v-if="!markdownContent && !loading" description="填写创作条件后开始生成" />
<template v-else>
<a-form layout="vertical">
@@ -201,11 +183,6 @@
</div>
</a-form-item>
</a-form>
<!-- <div v-if="responseSummary" class="response-meta">-->
<!-- <div class="meta-label">接口摘要</div>-->
<!-- <div class="meta-value">{{ responseSummary }}</div>-->
<!-- </div>-->
</template>
</a-card>
</a-col>
@@ -257,7 +234,6 @@
interface ParsedResult {
title: string;
content: string;
summary: string;
}
type DemoType = 'news' | 'wechat' | 'speech' | 'free';
@@ -278,7 +254,6 @@
const wsConnected = ref(false);
const generatedTitle = ref('');
const generatedContent = ref('');
const responseSummary = ref('');
const uploadedFiles = ref<Partial<FileRecord>[]>([]);
const activeConversationId = ref<string>();
const webSocketRef = ref<WebSocket | null>(null);
@@ -458,8 +433,7 @@
if (typeof payload === 'string') {
return {
title: form.theme.trim(),
content: payload,
summary: '接口直接返回文本内容'
content: payload
};
}
@@ -468,8 +442,7 @@
if (isFailedPayload(payload)) {
return {
title: form.theme.trim(),
content: FAILED_TEXT,
summary: '上游工作流执行失败'
content: FAILED_TEXT
};
}
@@ -497,10 +470,6 @@
payload.choices?.[0]?.message?.content
);
const summary = Object.keys(payload).length
? `已解析返回字段:${Object.keys(payload).join('、')}`
: '返回对象为空';
return {
title: title || form.theme.trim(),
// 不再 stringifyUnknown(payload):整包会带出 taskId / workflowRunId 等
@@ -509,8 +478,7 @@
payload,
scope: 'gxmu/manuscriptGen.parseResult',
candidates: []
}),
summary
})
};
};
@@ -532,14 +500,10 @@
return payload;
};
const applyParsedResult = (
payload: ManuscriptGenResult,
defaultSummary = '已通过 WebSocket 接收生成结果'
) => {
const applyParsedResult = (payload: ManuscriptGenResult) => {
const parsed = parseResult(payload);
generatedTitle.value = parsed.title || generatedTitle.value || form.theme.trim();
generatedContent.value = parsed.content;
responseSummary.value = parsed.summary || defaultSummary;
};
const shouldTreatAsDirectResult = (payload: unknown) => {
@@ -620,7 +584,6 @@
} else {
generatedContent.value += chunk;
}
responseSummary.value = '正在通过 WebSocket 接收流式内容';
};
const finishStream = (conversationId?: string) => {
@@ -628,7 +591,7 @@
activeConversationId.value = conversationId;
}
const payload = parseSerializedResult(generatedContent.value);
applyParsedResult(payload, '已通过 WebSocket 接收生成结果');
applyParsedResult(payload);
loading.value = false;
resolveStream();
disconnectWebSocket();
@@ -741,7 +704,6 @@
const clearGenerated = () => {
generatedTitle.value = '';
generatedContent.value = '';
responseSummary.value = '';
};
const resetForm = () => {
@@ -850,7 +812,6 @@
loading.value = true;
clearGenerated();
generatedTitle.value = form.theme.trim();
responseSummary.value = '正在提交生成任务...';
try {
// 一律走 HTTP 流式(/stream),不再先连 WebSocket:
// 触发生成的那个 POST 在整段生成期间是零字节的,会被链路上的 60s 空闲
@@ -858,9 +819,6 @@
// /stream 每 15s 有后端心跳续命,正文逐段喂给原来的帧处理,
// 结构化结果从末帧 {type:'final'} 取。
const payload = buildPayload();
responseSummary.value = templateMode.value
? '已提交生成任务,等待流式返回...'
: '自由创作已提交给 AI 创意助手,生成完成后一次性返回...';
const waitStream = createStreamWaiter();
const result = await runManuscriptGenStream(
payload as unknown as Record<string, unknown>,
@@ -871,8 +829,7 @@
applyParsedResult(
typeof result === 'string'
? parseSerializedResult(result)
: (result as ManuscriptGenResult),
'接口直接返回生成结果'
: (result as ManuscriptGenResult)
);
}
// 兜底:流结束但没收到 __END__ 时也别把页面卡在加载中
@@ -1107,13 +1064,6 @@
gap: 12px;
}
.upload-tip {
margin-top: 10px;
color: #5f7f9f;
font-size: 12px;
line-height: 1.7;
}
.file-list {
display: flex;
flex-direction: column;
@@ -1173,26 +1123,6 @@
color: #1590ff;
}
.hint-block {
margin-top: 18px;
padding: 14px 16px;
border-radius: 14px;
background: #f5fbff;
border: 1px solid #c7e8ff;
}
.hint-title {
margin-bottom: 6px;
font-weight: 600;
color: #1590ff;
}
.hint-text {
color: #5f7f9f;
line-height: 1.7;
font-size: 13px;
}
.result-card {
min-height: 100%;
}
@@ -1203,12 +1133,6 @@
gap: 10px;
}
.response-meta {
padding: 12px 14px;
border-radius: 14px;
background: #f5fbff;
}
.markdown-shell {
min-height: 360px;
border: 1px solid #c7e8ff;
@@ -1244,18 +1168,6 @@
color: #1f2d3d;
}
.meta-label {
margin-bottom: 4px;
font-weight: 600;
color: #1f2d3d;
}
.meta-value {
color: #5f7f9f;
line-height: 1.7;
word-break: break-all;
}
.full-width {
width: 100%;
}