feat: allow using chat to modify page titles

This commit is contained in:
LIlGG
2025-10-11 16:36:20 +08:00
parent a672fcad1c
commit 7acc4949fb
8 changed files with 88 additions and 23 deletions

View File

@@ -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<string, string[]>;
context?: Record<string, SelectContextResult>;
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('------')}
---
`;
}

View File

@@ -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:
你的回复应严格遵循以下格式:
---
<updateContextBuffer>
<selectPage pageName="pageName">
<selectPage pageName="pageName" pageTitle="pageTitle">
<selectSection>
...section content...
</selectSection>
@@ -61,8 +70,8 @@ export async function selectContext({
---
* 你应该从 <updateContextBuffer> 开始,以 </updateContextBuffer> 结束。
* 你可以在回复中包含多个 <selectPage> 标签,每个 <selectPage> 标签中也可以包含多个 <selectSection> 标签。
* 你需要在 <selectPage> 标签中包含页面名称,但每个页面名称只能出现一次。
* 你需要在 <selectSection> 标签中包含完整的 Section 内容,只做选择,但不要对 Section 内容进行任何修改。
* 你需要在 <selectPage> 标签中包含页面名称和页面标题,但每个页面只能出现一次。
* 你需要在 <selectSection> 标签中包含完整的 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<string, string[]> = {};
const selectedPages: Record<string, SelectContextResult> = {};
const selectPageRegex = /<selectPage\s+pageName="([^"]+)">([\s\S]*?)<\/selectPage>/g;
const selectPageRegex = /<selectPage\s+pageName="([^"]+)"\s+pageTitle="([^"]+)">([\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,
};
}
}

View File

@@ -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);

View File

@@ -171,6 +171,7 @@ ${allowedHTMLElements.map((tagName) => `<${tagName}>`).join(', ')}
- 必填 \`title\`:指定页面标题,使用对用户友好的名称作为标题,例如 "定价页面"、"联系我们" 等,在多页面中,请保持 title 不重复。
7. 使用编码最佳实践,页面应尽可能完善且满足用户要求。
8. 每个 \`uPageArtifact\` 生成完后,简洁地总结描述本次生成的内容。
9. 仅修改 artifact 自身属性时,无需使用 \`<uPageAction>\` 标签。
</artifact_instructions>
</artifact_info>
@@ -318,6 +319,17 @@ ${allowedHTMLElements.map((tagName) => `<${tagName}>`).join(', ')}
价格信息已更新基础版从¥99/月调整为¥129/月专业版从¥199/月调整为¥259/月。
</assistant_response>
</example>
<example>
<user_query>修改 index 页面的标题为“个人主页”</user_query>
<assistant_response>
<uPageArtifact id="page-index" name="index" title="个人主页">
</uPageArtifact>
已更新 index 页面的标题为“个人主页”。
</assistant_response>
</example>
<examples>
`;

View File

@@ -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 {

View File

@@ -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<ArtifactUpdateState>) {

View File

@@ -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;
}

View File

@@ -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<string, string[]>; pageSummary: string } = {
const minorModelData: { summary: string; context: Record<string, SelectContextResult>; pageSummary: string } = {
summary: '',
context: {},
pageSummary: '',