@@ -13,6 +13,10 @@ export async function parseStreamErrorResponse(response: Response): Promise { try { @@ -40,10 +44,21 @@ export const chatMessagesApi = { sendMessage: async (chatUid: string, dataForSend: MessageSend | FormData, token?: string) => { const headerDataType = dataForSend instanceof FormData ? 'multipart/form-data' : 'application/json' + const payload = + dataForSend instanceof FormData + ? dataForSend + : { ...dataForSend, content: sanitizeContent(dataForSend.content) } + + if (payload instanceof FormData && payload.has('content')) { + const raw = payload.get('content') + if (typeof raw === 'string') { + payload.set('content', sanitizeContent(raw)) + } + } return axios.post>( getApiUrl() + `/chats/${chatUid}/messages/`, - dataForSend, + payload, { withCredentials: true, headers: { @@ -62,6 +77,12 @@ export const chatMessagesApi = { } if (dataForSend instanceof FormData) { + if (dataForSend.has('content')) { + const raw = dataForSend.get('content') + if (typeof raw === 'string') { + dataForSend.set('content', sanitizeContent(raw)) + } + } return fetch(url, { method: 'POST', headers: streamHeaders, @@ -71,7 +92,8 @@ export const chatMessagesApi = { }) } - const { content, info } = dataForSend + const { info } = dataForSend + const content = sanitizeContent(dataForSend.content) return fetch(url, { method: 'POST', @@ -47,12 +47,12 @@ function buildRequiredForVersion(inputs: IModelInputs[], currentVersion: string) function composeMessage(text: string, quote: string | null | undefined): string { const parts: string[] = [] if (quote != null && quote !== '') { - parts.push('```\n' + quote + '\n```') + parts.push('```' + quote + '```') } if (text !== '') { parts.push(text) } - return parts.join('\n') + return parts.join(' ') } interface Input { @@ -266,6 +266,11 @@ export const ModelInput: FC = ({ target.style.height = target.scrollHeight + 'px' }} onKeyDown={async (e) => { + if (e.key === 'Backspace' && hasQuote && value === '') { + e.preventDefault() + onQuoteChange?.(null) + return + } if (e.shiftKey && e.key === 'Enter') { } else if (e.key === 'Enter') { e.preventDefault() @@ -344,15 +349,15 @@ export const ModelInput: FC = ({ loading={loading} unpinImage={unpinImage} input={composedForSend} - sendMessage={(msg, req) => wrappedSendMessage(msg, req)} - setInput={externalOnChange ? + sendMessage={() => submitMessage()} + setInput={externalOnChange ? (val: string | ((prev: string) => string)) => { const newVal = typeof val === 'function' ? val(value) : val externalOnChange(newVal) if (newVal === '') { onQuoteChange?.(null) } - } : + } : (val: string | ((prev: string) => string)) => { const next = typeof val === 'function' ? val(value) : val if (next === '') { @@ -9,7 +9,8 @@ export function parseCodeFenceSegments(value: string): ContentSegment[] { } const segments: ContentSegment[] = [] - const re = /```[^\n]*\r?\n([\s\S]*?)```/g + // Многострочный ```\ncode\n``` и однострочный ```code``` (как уходит на бэк) + const re = /```[^\n]*\r?\n([\s\S]*?)```|```([^`\n]+)```/g let lastIndex = 0 let match: RegExpExecArray | null @@ -18,7 +19,7 @@ export function parseCodeFenceSegments(value: string): ContentSegment[] { segments.push({ type: 'text', content: value.slice(lastIndex, match.index) }) } - let code = match[1] + let code = match[1] ?? match[2] ?? '' if (code.endsWith('\n')) { code = code.slice(0, -1) } @@ -30,11 +30,8 @@ export const SendBtn = ({ loading, unpinImage, input, sendMessage, setInput, req id='text-models-tour-4' onClick={async () => { if (!loading) { - const isSend = sendMessage(input, required) - if (isSend) { - setInput('') - if (unpinImage) unpinImage() - } + // Parent (submitMessage) already clears draft / quote / image on success + sendMessage(input, required) } }} height={28}