From 7acc4949fb4d76a2c5429769ae3d1289ac07fcc5 Mon Sep 17 00:00:00 2001 From: LIlGG <1103069291@qq.com> Date: Sat, 11 Oct 2025 16:36:20 +0800 Subject: [PATCH] feat: allow using chat to modify page titles --- app/lib/.server/llm/chat-stream-text.ts | 15 ++++++++--- app/lib/.server/llm/select-context.ts | 36 +++++++++++++++++-------- app/lib/bridge/index.ts | 20 ++++++++++++++ app/lib/common/prompts/prompts.ts | 12 +++++++++ app/lib/hooks/useProject.ts | 11 +++++--- app/lib/stores/chat.ts | 4 ++- app/lib/stores/pages.ts | 9 ++++++- app/routes/api.chat/chat.server.ts | 4 +-- 8 files changed, 88 insertions(+), 23 deletions(-) diff --git a/app/lib/.server/llm/chat-stream-text.ts b/app/lib/.server/llm/chat-stream-text.ts index fe43fbd..e5cce52 100644 --- a/app/lib/.server/llm/chat-stream-text.ts +++ b/app/lib/.server/llm/chat-stream-text.ts @@ -12,13 +12,14 @@ import type { ElementInfo } from '~/routes/api.chat/chat.server'; import type { UPageUIMessage } from '~/types/message'; import { approximatePromptTokenCount, encode } from '~/utils/token'; import { MAX_TOKENS } from './constants'; +import type { SelectContextResult } from './select-context'; import { tools } from './tools'; export type ChatStreamTextProps = CallSettings & { messages: UPageUIMessage[]; summary: string; pageSummary: string; - context?: Record; + context?: Record; model: LanguageModel; maxTokens?: number; elementInfo?: ElementInfo; @@ -62,12 +63,18 @@ ${summary} if (context) { systemPrompt = `${systemPrompt} -以下是根据用户的聊天记录和任务分析出的可能对此次任务有帮助的代码片段,按页面名称区分 +以下是根据用户的聊天记录和任务分析出的可能对此次任务有帮助的页面及其代码片段,按页面名称区分,多个页面使用 ------ 分割 CONTEXT: --- ${Object.entries(context) - .map(([key, value]) => `${key}: ${value.join('\n')}\n`) - .join('\n')} + .map( + ([key, value]) => ` + - 页面名称: ${value.pageName} + - 页面标题: ${value.pageTitle} + - 页面内容: ${value.sections.join('\n')} + `, + ) + .join('------')} --- `; } diff --git a/app/lib/.server/llm/select-context.ts b/app/lib/.server/llm/select-context.ts index 3eea8e5..bbe637f 100644 --- a/app/lib/.server/llm/select-context.ts +++ b/app/lib/.server/llm/select-context.ts @@ -6,6 +6,12 @@ import type { UPageUIMessage } from '~/types/message'; const logger = createScopedLogger('select-context'); +export type SelectContextResult = { + sections: string[]; + pageName: string; + pageTitle: string; +}; + export async function selectContext({ messages, pages, @@ -31,26 +37,29 @@ export async function selectContext({ --- 页面名称:${page.name} --- + 页面标题:${page.title} + --- 页面内容:${page.content} `; }); const resp = await generateText({ system: ` - 你是一名软件工程师。你正在从事一个 HTML 项目,该项目包含多个页面,每个页面内容中包含多个 Section。这些 Section 可能是 HTML、style、JavaScript 片段。 - 提供给你的为 Body 内容,每个处于根节点下的 HTML 标签,都包含一个唯一的 domId 属性,并且为单独的一个 Section。 + 你是一名软件工程师。你正在从事一个 HTML 项目,该项目包含多个页面,每个页面内容中包含 HTML 片段,可能是 HTML、style、JavaScript 片段。 ${pagesContent.join('\n')} --- - 现在,你将获得一个任务。你需要从上述页面列表中选择与任务相关的页面与其相关的 Section。 + 现在,你将获得一个任务。你需要从上述页面列表中选择与任务相关的页面与用户任务相关的 HTML 片段。请务必保证: + - 如果涉及到脚本,则需要选择可能相关的所有脚本的完整内容。 + - 如果涉及到样式,则需要选择可能相关的所有样式,包括内联样式、外部样式表和 style 标签中的样式。 RESPONSE FORMAT: 你的回复应严格遵循以下格式: --- - + ...section content... @@ -61,8 +70,8 @@ export async function selectContext({ --- * 你应该从 开始,以 结束。 * 你可以在回复中包含多个 标签,每个 标签中也可以包含多个 标签。 - * 你需要在 标签中包含页面名称,但每个页面名称只能出现一次。 - * 你需要在 标签中包含完整的 Section 内容,只做选择,但不要对 Section 内容进行任何修改。 + * 你需要在 标签中包含页面名称和页面标题,但每个页面只能出现一次。 + * 你需要在 标签中包含完整的 HTML 内容,只做选择,但不要对 HTML 内容进行任何修改。 * 如果不需要任何更改,你可以留下空的 updateContextBuffer 标签。 `, prompt: ` @@ -70,7 +79,7 @@ export async function selectContext({ 用户当前任务: ${extractTextContent(lastUserMessage)} - 请根据当前页面与 Section 的详细代码,选择与任务相关的页面以及 Section。 + 请根据当前页面、页面属性以及内容,选择与任务相关的页面以及 HTML 片段。 `, model, abortSignal, @@ -84,14 +93,15 @@ export async function selectContext({ } const updateContextBufferContent = updateContextBuffer[1]; - const selectedPages: Record = {}; + const selectedPages: Record = {}; - const selectPageRegex = /([\s\S]*?)<\/selectPage>/g; + const selectPageRegex = /([\s\S]*?)<\/selectPage>/g; let selectPageMatch; while ((selectPageMatch = selectPageRegex.exec(updateContextBufferContent)) !== null) { const pageName = selectPageMatch[1]; - const pageContent = selectPageMatch[2]; + const pageTitle = selectPageMatch[2]; + const pageContent = selectPageMatch[3]; if (!pageName) { logger.warn('页面名称为空'); @@ -110,7 +120,11 @@ export async function selectContext({ } if (sections.length > 0) { - selectedPages[pageName] = sections; + selectedPages[pageName] = { + sections, + pageName, + pageTitle, + }; } } diff --git a/app/lib/bridge/index.ts b/app/lib/bridge/index.ts index f22a4f1..b86d988 100644 --- a/app/lib/bridge/index.ts +++ b/app/lib/bridge/index.ts @@ -101,6 +101,26 @@ export class EditorBridge { }); } + /** + * 更新页面属性 + * + * @param pageName + * @param param1 + * @returns + */ + async updatePageAttributes(pageName: string, { title }: { title?: string } = {}) { + const page = this.#pages.get(pageName); + if (!page) { + return; + } + const actionIds = page.actionIds; + this.#emit('upsert_page', { + pageName, + title, + actionIds, + }); + } + async removePage(pageName: string) { this.#pages.delete(pageName); diff --git a/app/lib/common/prompts/prompts.ts b/app/lib/common/prompts/prompts.ts index f89e713..85fbcfc 100644 --- a/app/lib/common/prompts/prompts.ts +++ b/app/lib/common/prompts/prompts.ts @@ -171,6 +171,7 @@ ${allowedHTMLElements.map((tagName) => `<${tagName}>`).join(', ')} - 必填 \`title\`:指定页面标题,使用对用户友好的名称作为标题,例如 "定价页面"、"联系我们" 等,在多页面中,请保持 title 不重复。 7. 使用编码最佳实践,页面应尽可能完善且满足用户要求。 8. 每个 \`uPageArtifact\` 生成完后,简洁地总结描述本次生成的内容。 + 9. 仅修改 artifact 自身属性时,无需使用 \`\` 标签。 @@ -318,6 +319,17 @@ ${allowedHTMLElements.map((tagName) => `<${tagName}>`).join(', ')} 价格信息已更新,基础版从¥99/月调整为¥129/月,专业版从¥199/月调整为¥259/月。 + + + 修改 index 页面的标题为“个人主页” + + + + + + 已更新 index 页面的标题为“个人主页”。 + + `; diff --git a/app/lib/hooks/useProject.ts b/app/lib/hooks/useProject.ts index 1bd0778..44460a4 100644 --- a/app/lib/hooks/useProject.ts +++ b/app/lib/hooks/useProject.ts @@ -49,10 +49,13 @@ export function useProject() { return true; }); if (!isConsistent) { - logger.error('保存项目失败: 页面内容与 actions 不一致', { - projectPages, - projectSections, - }); + logger.error( + '保存项目失败: 页面内容与 actions 不一致', + JSON.stringify({ + projectPages, + projectSections, + }), + ); return false; } try { diff --git a/app/lib/stores/chat.ts b/app/lib/stores/chat.ts index 787aa1b..513709c 100644 --- a/app/lib/stores/chat.ts +++ b/app/lib/stores/chat.ts @@ -96,7 +96,7 @@ export class ChatStore { }); } - addArtifact({ messageId, name, title, id }: ArtifactCallbackData) { + async addArtifact({ messageId, name, title, id }: ArtifactCallbackData) { const artifact = this.getArtifact(messageId, name); if (artifact) { return; @@ -129,6 +129,8 @@ export class ChatStore { artifactsByPageName.set(name, newArtifact); this.artifacts.set(artifactsByMessageId); + const bridge = await editorBridge; + bridge.updatePageAttributes(name, { title }); } updateArtifact({ messageId, name }: ArtifactCallbackData, state: Partial) { diff --git a/app/lib/stores/pages.ts b/app/lib/stores/pages.ts index f15bcfe..65d9c7c 100644 --- a/app/lib/stores/pages.ts +++ b/app/lib/stores/pages.ts @@ -299,6 +299,11 @@ export class PagesStore { switch (type) { case 'add_page': { const { title: pageTitle, actionIds = [] } = payload; + const oldPage = this.pages.get()[pageName]; + if (oldPage) { + throw new Error(`Page ${pageName} already exists`); + } + this.pages.setKey(pageName, { name: pageName, title: pageTitle, @@ -312,10 +317,12 @@ export class PagesStore { } case 'upsert_page': { const { title: pageTitle, actionIds = [] } = payload; + const oldPage = this.pages.get()[pageName]; this.pages.setKey(pageName, { name: pageName, title: pageTitle, - actionIds, + actionIds: actionIds || oldPage?.actionIds, + content: oldPage?.content, }); break; } diff --git a/app/routes/api.chat/chat.server.ts b/app/routes/api.chat/chat.server.ts index 4894d49..56e6a15 100644 --- a/app/routes/api.chat/chat.server.ts +++ b/app/routes/api.chat/chat.server.ts @@ -11,7 +11,7 @@ import { ChatUsageStatus, recordUsage, updateUsageStatus } from '~/lib/.server/c import { chatStreamText } from '~/lib/.server/llm/chat-stream-text'; import { MAX_RESPONSE_SEGMENTS, MAX_TOKENS } from '~/lib/.server/llm/constants'; import { createSummary } from '~/lib/.server/llm/create-summary'; -import { selectContext } from '~/lib/.server/llm/select-context'; +import { type SelectContextResult, selectContext } from '~/lib/.server/llm/select-context'; import { structuredPageSnapshot } from '~/lib/.server/llm/structured-page-snapshot'; import { createScopedLogger } from '~/lib/.server/logger'; import { getHistoryChatMessages, saveChatMessages, updateDiscardedMessage } from '~/lib/.server/message'; @@ -157,7 +157,7 @@ export async function chatAction({ request, userId }: ChatActionArgs) { }); // 辅助 model 所获取的数据,用于后续的模型调用。 - const minorModelData: { summary: string; context: Record; pageSummary: string } = { + const minorModelData: { summary: string; context: Record; pageSummary: string } = { summary: '', context: {}, pageSummary: '',