diff --git a/web/gen/orpc/common.gen.ts b/web/gen/orpc/common.gen.ts index d26eae0d18..8080acb31c 100644 --- a/web/gen/orpc/common.gen.ts +++ b/web/gen/orpc/common.gen.ts @@ -18,30 +18,34 @@ import { textToAudioChatContract } from './api/text-to-audio.gen' export const base = oc.$route({ inputStructure: 'detailed', outputStructure: 'detailed' }) export const contracts = { - sendChatMessageContract, - uploadChatFileContract, - previewChatFileContract, - stopChatMessageGenerationContract, - postChatMessageFeedbackContract, - getChatAppFeedbacksContract, - getSuggestedQuestionsContract, - getConversationHistoryContract, - getConversationsListContract, - deleteConversationContract, - renameConversationContract, - getConversationVariablesContract, - audioToTextContract, - textToAudioChatContract, - getChatAppInfoContract, - getChatAppParametersContract, - getChatAppMetaContract, - getChatWebAppSettingsContract, - getAnnotationListContract, - createAnnotationContract, - deleteAnnotationContract, - updateAnnotationContract, - initialAnnotationReplySettingsContract, - getInitialAnnotationReplySettingsStatusContract, + chatMessages: { send: sendChatMessageContract, stopGeneration: stopChatMessageGenerationContract }, + files: { upload: uploadChatFileContract, preview: previewChatFileContract }, + messages: { + postFeedback: postChatMessageFeedbackContract, + getSuggestedQuestions: getSuggestedQuestionsContract, + getConversationHistory: getConversationHistoryContract, + }, + app: { getFeedbacks: getChatAppFeedbacksContract }, + conversations: { + getList: getConversationsListContract, + delete: deleteConversationContract, + rename: renameConversationContract, + getVariables: getConversationVariablesContract, + }, + audioToText: { audioToText: audioToTextContract }, + textToAudio: { textToAudio: textToAudioChatContract }, + info: { get: getChatAppInfoContract }, + parameters: { get: getChatAppParametersContract }, + meta: { get: getChatAppMetaContract }, + site: { getSettings: getChatWebAppSettingsContract }, + apps: { + getAnnotationList: getAnnotationListContract, + createAnnotation: createAnnotationContract, + deleteAnnotation: deleteAnnotationContract, + updateAnnotation: updateAnnotationContract, + initialAnnotationReplySettings: initialAnnotationReplySettingsContract, + getInitialAnnotationReplySettingsStatus: getInitialAnnotationReplySettingsStatusContract, + }, } export type Contracts = typeof contracts diff --git a/web/plugins/hey-api-orpc/plugin.ts b/web/plugins/hey-api-orpc/plugin.ts index 9d45c865ed..e1439f958a 100644 --- a/web/plugins/hey-api-orpc/plugin.ts +++ b/web/plugins/hey-api-orpc/plugin.ts @@ -7,6 +7,76 @@ function capitalizeFirst(str: string): string { return str.charAt(0).toUpperCase() + str.slice(1) } +// Convert kebab-case to camelCase: "chat-messages" → "chatMessages" +function toCamelCase(str: string): string { + return str.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase()) +} + +// Extract first path segment: "/chat-messages" → "chat-messages" +function getPathSegment(path: string): string { + return path.split('/').filter(Boolean)[0] || 'common' +} + +// Simplify operation key by removing redundant parts based on the group +// e.g., "sendChatMessage" with segment "chat-messages" → "send" +// e.g., "getConversationsList" with segment "conversations" → "list" +// e.g., "uploadChatFile" with segment "files" → "upload" +function simplifyOperationKey(operationId: string, segment: string): string { + // Patterns to remove (order matters - more specific first) + const patternsToRemove = [ + // App-specific patterns + 'ChatWebApp', + 'ChatApp', + 'ChatFile', + 'ChatMessage', + 'Chat', + // Segment-based patterns + ...buildSegmentPatterns(segment), + ] + + let simplified = operationId + + // Remove patterns iteratively + for (const pattern of patternsToRemove) { + const regex = new RegExp(pattern, 'g') + const result = simplified.replace(regex, '') + if (result !== simplified && result.length > 0) { + simplified = result + } + } + + // Ensure first char is lowercase + simplified = simplified.charAt(0).toLowerCase() + simplified.slice(1) + + // Handle edge cases where we end up with just HTTP method + // e.g., "get" → keep as "get", but "getChatApp" → "get" is fine for single operations + if (!simplified || simplified.length < 2) { + return operationId.charAt(0).toLowerCase() + operationId.slice(1) + } + + return simplified +} + +// Build patterns from segment name +// "chat-messages" → ["ChatMessages", "ChatMessage"] +// "conversations" → ["Conversations", "Conversation"] +// "audio-to-text" → ["AudioToText"] +function buildSegmentPatterns(segment: string): string[] { + const parts = segment.split('-') + const patterns: string[] = [] + + // Full camelCase: "chat-messages" → "ChatMessages" + const fullCamel = parts.map(capitalizeFirst).join('') + patterns.push(fullCamel) + + // Singular form: "ChatMessages" → "ChatMessage" + if (fullCamel.endsWith('s') && !fullCamel.endsWith('ss')) { + patterns.push(fullCamel.slice(0, -1)) + } + + return patterns +} + function toZodSchemaName(operationId: string, type: 'data' | 'response'): string { const pascalName = capitalizeFirst(operationId) return type === 'data' ? `z${pascalName}Data` : `z${pascalName}Response` @@ -225,7 +295,7 @@ export const handler: OrpcPlugin['Handler'] = ({ plugin }) => { plugin.node(contractNode) } - // Create contracts object export + // Create contracts object export grouped by API path segment const contractsSymbol = plugin.symbol('contracts', { exported: true, meta: { @@ -233,13 +303,31 @@ export const handler: OrpcPlugin['Handler'] = ({ plugin }) => { }, }) - const contractsObject = $.object() + // Group operations by path segment + const operationsBySegment = new Map() for (const op of operations) { - const contractSymbol = contractSymbols[op.id] - if (contractSymbol) { - const contractName = contractNameBuilder(op.id) - contractsObject.prop(contractName, $(contractSymbol)) + const segment = getPathSegment(op.path) + if (!operationsBySegment.has(segment)) { + operationsBySegment.set(segment, []) } + operationsBySegment.get(segment)!.push(op) + } + + // Build nested contracts object + const contractsObject = $.object() + for (const [segment, segmentOps] of operationsBySegment) { + const groupKey = toCamelCase(segment) + const groupObject = $.object() + + for (const op of segmentOps) { + const contractSymbol = contractSymbols[op.id] + if (contractSymbol) { + const key = simplifyOperationKey(op.id, segment) + groupObject.prop(key, $(contractSymbol)) + } + } + + contractsObject.prop(groupKey, groupObject) } const contractsNode = $.const(contractsSymbol)