返回 oh-my-ppt
chatSendUtils.ts
根目录 / src / renderer / src / components / session-detail / hooks / chatSendUtils.ts
1 import type {
2 ChatSendContext,
3 ChatSendGuardInput,
4 ResolveChatSendContextInput,
5 SessionPreviewPage,
6 SessionDetailChatType
7 } from '@renderer/types/session-detail'
8
9 export type MainSessionEditResolution =
10 | { ready: true; selectPageIds: string[] }
11 | { ready: false; reason: 'page-structure' | 'page-not-found' }
12
13 const ALL_PAGES_PATTERN = /\b(all|every|entire)\b|全部|所有|整套|全套|每一页|每页/i
14 const CURRENT_PAGE_PATTERN = /当前页|这一页|本页|current\s+(?:page|slide)|this\s+(?:page|slide)/i
15 const EXPLICIT_PAGE_PATTERN =
16 /第\s*(?:\d+|[零一二两三四五六七八九十百]+)\s*页|\b(?:p|page|slide)\s*[-#]?\s*\d+\b|\/[a-z0-9_-]+\.html|当前页|这一页|本页|current\s+(?:page|slide)|this\s+(?:page|slide)/i
17 const PAGE_NUMBER_SOURCE = '(?:\\d+|[零一二两三四五六七八九十百]+)'
18 const PAGE_CONTENT_TARGET_PATTERN =
19 /(?:页面|页)\s*(?:的\s*)?(?:标题|副标题|内容|文案|文字|图表|图片|元素|配色|颜色|样式|布局|背景)|(?:page|slide)\s*(?:['’]s\s*)?(?:title|heading|subtitle|content|copy|text|chart|image|element|color|style|layout|background)\b/i
20
21 const parseChinesePageNumber = (value: string): number | null => {
22 const digits: Record<string, number> = {
23 零: 0,
24 一: 1,
25 二: 2,
26 两: 2,
27 三: 3,
28 四: 4,
29 五: 5,
30 六: 6,
31 七: 7,
32 八: 8,
33 九: 9
34 }
35 const units: Record<string, number> = { 十: 10, 百: 100 }
36 let total = 0
37 let digit: number | null = null
38 let previousUnit = Number.POSITIVE_INFINITY
39
40 for (const character of value) {
41 if (character in digits) {
42 digit = digits[character]
43 continue
44 }
45 const unit = units[character]
46 if (!unit || unit >= previousUnit) return null
47 total += (digit ?? 1) * unit
48 digit = null
49 previousUnit = unit
50 }
51 return total + (digit ?? 0)
52 }
53
54 const parsePageNumber = (value: string): number | null =>
55 /^\d+$/.test(value) ? Number(value) : parseChinesePageNumber(value)
56
57 export function isUnsupportedMainSessionPageStructureRequest(userMessage: string): boolean {
58 const text = userMessage.trim()
59 if (PAGE_CONTENT_TARGET_PATTERN.test(text)) return false
60 return (
61 /(?:新增|添加|插入|复制)\s*(?:(?:一|两|几|\d+)\s*)?(?:个|张)?\s*(?:新)?(?:页|页面)/i.test(
62 text
63 ) ||
64 /(?:删除|移除|删掉)\s*(?:(?:第\s*)?(?:\d+|[零一二两三四五六七八九十百]+)\s*(?:页|页面)|当前页|这一页|本页)(?:\s*$|[,。,.!?!?])/i.test(
65 text
66 ) ||
67 /(?:调整|修改|移动|重排|重新排列).{0,8}(?:页面|页)(?:的)?顺序/i.test(text) ||
68 /(?:add|insert|duplicate|create)\s+(?:(?:a|an|one|\d+)\s+)?(?:new\s+)?(?:page|slide)\b/i.test(
69 text
70 ) ||
71 /(?:delete|remove)\s+(?:page|slide)\s*\d+\s*[.!?]?$/i.test(text) ||
72 /(?:move|reorder)\s+(?:the\s+)?(?:pages?|slides?)(?:\s+order)?\b/i.test(text)
73 )
74 }
75
76 export function resolveMainSessionEdit(
77 userMessage: string,
78 pages: Array<Pick<SessionPreviewPage, 'pageId' | 'pageNumber'>>,
79 selectedPageId: string | undefined,
80 requestedPageIds: string[] = []
81 ): MainSessionEditResolution {
82 if (isUnsupportedMainSessionPageStructureRequest(userMessage)) {
83 return { ready: false, reason: 'page-structure' }
84 }
85
86 const pageById = new Map(pages.map((page) => [page.pageId.toLowerCase(), page]))
87 const pageByNumber = new Map(pages.map((page) => [page.pageNumber, page]))
88 const selectedIds = Array.from(
89 new Set(
90 requestedPageIds
91 .map((pageId) => pageById.get(pageId.toLowerCase())?.pageId)
92 .filter((pageId): pageId is string => Boolean(pageId))
93 )
94 )
95 if (selectedIds.length > 0 || requestedPageIds.length > 0) {
96 return selectedIds.length > 0
97 ? { ready: true, selectPageIds: selectedIds }
98 : { ready: false, reason: 'page-not-found' }
99 }
100
101 const pageIds = new Set<string>()
102 const addPageNumber = (pageNumber: number): void => {
103 const page = pageByNumber.get(pageNumber)
104 if (page) pageIds.add(page.pageId)
105 }
106
107 if (CURRENT_PAGE_PATTERN.test(userMessage) && selectedPageId) pageIds.add(selectedPageId)
108
109 for (const match of userMessage.matchAll(/\/([a-z0-9_-]+)\.html/gi)) {
110 const page = pageById.get(match[1].toLowerCase())
111 if (page) pageIds.add(page.pageId)
112 }
113
114 const rangePattern = new RegExp(
115 `(?:第\\s*|(?:p|page|slide)\\s*)?(${PAGE_NUMBER_SOURCE})\\s*(?:页\\s*)?(?:到|至|[-~—–])\\s*(?:第\\s*|(?:p|page|slide)\\s*)?(${PAGE_NUMBER_SOURCE})\\s*(?:页|pages?|slides?)?`,
116 'gi'
117 )
118 for (const match of userMessage.matchAll(rangePattern)) {
119 const start = parsePageNumber(match[1])
120 const end = parsePageNumber(match[2])
121 if (start === null || end === null || Math.abs(end - start) > pages.length) continue
122 const direction = start <= end ? 1 : -1
123 for (let pageNumber = start; pageNumber !== end + direction; pageNumber += direction) {
124 addPageNumber(pageNumber)
125 }
126 }
127
128 const pageReferencePattern = new RegExp(
129 `第\\s*(${PAGE_NUMBER_SOURCE})\\s*页|\\b(?:p|page|slide)\\s*[-#]?\\s*(\\d+)\\b`,
130 'gi'
131 )
132 for (const match of userMessage.matchAll(pageReferencePattern)) {
133 const pageNumber = parsePageNumber(match[1] || match[2])
134 if (pageNumber !== null) addPageNumber(pageNumber)
135 }
136
137 for (const match of userMessage.matchAll(/第\s*([\d\s、,,和与及~-]+)\s*页/g)) {
138 for (const pageNumber of match[1].matchAll(/\d+/g)) addPageNumber(Number(pageNumber[0]))
139 }
140
141 if (pageIds.size > 0) return { ready: true, selectPageIds: Array.from(pageIds) }
142 if (ALL_PAGES_PATTERN.test(userMessage)) return { ready: true, selectPageIds: [] }
143 if (EXPLICIT_PAGE_PATTERN.test(userMessage)) return { ready: false, reason: 'page-not-found' }
144 return { ready: true, selectPageIds: [] }
145 }
146
147 export function isChatSendBlocked(input: ChatSendGuardInput): boolean {
148 return (
149 !input.sessionId ||
150 input.sending ||
151 input.generating ||
152 (!input.input.trim() && input.pendingAssetCount === 0)
153 )
154 }
155
156 export function resolveChatSendContext(input: ResolveChatSendContextInput): ChatSendContext {
157 const selector = input.selectedSelector?.trim() || ''
158 const hasSelector = selector.length > 0
159 const chatType: SessionDetailChatType = hasSelector ? 'page' : input.chatType
160 const targetPage = input.selectedPage ?? input.firstPage
161
162 if (chatType === 'page') {
163 if (!targetPage?.id) return { ready: false }
164 return {
165 ready: true,
166 hasSelector,
167 selector: hasSelector ? selector : null,
168 chatType,
169 targetPageId: targetPage.id,
170 targetPagePath: targetPage.htmlPath || input.firstPage?.htmlPath,
171 messagePageId: targetPage.id
172 }
173 }
174
175 return {
176 ready: true,
177 hasSelector,
178 selector: null,
179 chatType,
180 messagePageId: null
181 }
182 }
183
183 lines TYPESCRIPT