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