Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -81,13 +81,13 @@ describe('call-pair schema', () => {
describe('fieldName validation', () => {
it.each([
'field1',
'field_name',
'field-name',
'Field123',
'field_123-test',
'ABC123_test-name',
'field name',
'field 123',
'ABC 123 Test',
'field with multiple spaces',
])(
'should accept valid field names with letters, numbers, underscores, and hyphens: %s',
'should accept valid field names with letters, numbers, and spaces: %s',
(fieldName) => {
const result = schema.safeParse({
promptType: 'analyse',
Expand All @@ -100,12 +100,15 @@ describe('call-pair schema', () => {
],
})
assert(result.success === true)
assert(result.data.responseFields[0].fieldName === fieldName)
const parsedField = result.data.responseFields[0]
assert(parsedField.fieldName === fieldName.replace(/\s/g, '_'))
assert(parsedField.originalFieldName === fieldName)
},
)

it.each([
'field name',
'field_name',
'field-name',
'field@name',
'field.name',
'field#name',
Expand Down Expand Up @@ -173,11 +176,11 @@ describe('call-pair schema', () => {
prompt: 'test prompt',
responseFields: [
{
fieldName: 'Field1',
fieldName: 'Field 1',
fieldType: 'text',
},
{
fieldName: 'field1',
fieldName: 'field 1',
fieldType: 'text',
},
{
Expand Down Expand Up @@ -453,7 +456,7 @@ describe('call-pair schema', () => {
fieldType: 'text',
},
{
fieldName: 'confidence_score',
fieldName: 'confidence score',
fieldType: 'number',
},
],
Expand Down
19 changes: 7 additions & 12 deletions packages/backend/src/apps/pair/actions/process-image/schema.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import z from 'zod/v3'

import {
outputFieldNameSchema,
toSanitizedOutputFieldNamePair,
} from '../../common/schema'

export const schema = z.object({
// NOTE: this is an array because the attachment field returns an array
image: z
Expand All @@ -12,16 +17,7 @@ export const schema = z.object({
.array(
z
.object({
fieldName: z
.string()
.min(1, { message: 'Output name is required' })
.max(64, {
message: 'Output name cannot be more than 64 characters',
})
.regex(/^[a-zA-Z0-9\s]+$/, {
message:
'Output name can only contain letters, numbers, and spaces',
}),
fieldName: outputFieldNameSchema,

description: z
.string()
Expand All @@ -31,8 +27,7 @@ export const schema = z.object({
}),
})
.transform((field) => ({
fieldName: field.fieldName.replace(/\s/g, '_'),
originalFieldName: field.fieldName,
...toSanitizedOutputFieldNamePair(field.fieldName),
description: field.description,
})),
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ async function getDataOutMetadata(

const metadata = Object.keys(dataOut).reduce((acc, key) => {
acc[key] = {
label: key,
label: key.replace(/_/g, ' '),
type: 'ai_response',
}
return acc
Expand Down
52 changes: 25 additions & 27 deletions packages/backend/src/apps/pair/actions/send-prompt/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { schema } from './schema'
const turndownService = new TurndownService()

const action: IRawAction = {
name: 'Ask Pair',
name: 'Use Pair',
key: 'sendPrompt',
description:
'Enter a custom prompt to summarise, categorise or analyse data with Pair',
Expand All @@ -42,50 +42,39 @@ const action: IRawAction = {
showOptionValue: false,
},
{
label: 'Prompt',
label: 'Describe what you want Pair to do',
key: 'prompt',
type: 'rich-text' as const,
required: true,
variables: true,
customRteMenuOptions: [
'Bold',
'Italic',
'Underline',
'Divider', // specify when to show a divider
'Heading1',
'Heading2',
'Heading3',
'Heading4',
'ListBullet',
'ListOrdered',
'Divider',
'Undo',
'Redo',
],
/**
* TODO (kevinkim-ogp): monitor this feature to see if we
* need to add the custom RTE menu options back.
*
* The initial release uses the RTE so users can still format
* using keyboard shortcuts, but hides the menu bar to keep
* this feature simple.
*/
customRteMenuOptions: [],
defaultValue: {
fieldKey: 'promptType',
options: DEFAULT_PROMPT_VALUES,
},
},
{
label: 'How do you want the response?',
label: 'Define how you want Pair to structure what it extracts',
description: 'Use these as variables in later steps',
key: 'responseFields',
type: 'multirow-multicol' as const,
required: true,
hiddenIf: {
fieldKey: 'promptType',
op: 'is_empty',
},
addRowButtonText: 'Add output',
subFields: [
{
placeholder: 'Field name',
key: 'fieldName',
type: 'string' as const,
required: true,
variables: false,
},
{
placeholder: 'Field type',
label: 'Type',
key: 'fieldType',
type: 'dropdown' as const,
required: true,
Expand All @@ -97,7 +86,16 @@ const action: IRawAction = {
],
},
{
placeholder: 'Categories (comma-separated)',
label: 'Output name',
placeholder: 'E.g., Priority level',
key: 'fieldName',
type: 'string' as const,
required: true,
variables: false,
},
{
label: 'Categories',
placeholder: 'Separated by commas',
key: 'fieldCategories',
type: 'string' as const,
variables: true,
Expand Down
19 changes: 11 additions & 8 deletions packages/backend/src/apps/pair/actions/send-prompt/schema.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,26 @@
import z from 'zod/v3'

import {
outputFieldNameSchema,
toSanitizedOutputFieldNamePair,
} from '../../common/schema'

export const schema = z.object({
promptType: z.enum(['analyse', 'categorise', 'summarise', 'write', 'custom']),
prompt: z.string().min(1),
responseFields: z
.array(
z
.object({
fieldName: z
.string()
.min(1)
.max(64)
.regex(
/^[a-zA-Z0-9_-]+$/,
'Field name cannot contain spaces. Use only letters, numbers, underscores (_), and hyphens (-)',
),
fieldName: outputFieldNameSchema,
fieldType: z.enum(['text', 'number', 'category']),
fieldCategories: z.string().optional(),
})
.transform((field) => ({
...toSanitizedOutputFieldNamePair(field.fieldName),
fieldType: field.fieldType,
fieldCategories: field.fieldCategories,
}))
.refine(
(data: {
fieldName?: string
Expand Down
32 changes: 32 additions & 0 deletions packages/backend/src/apps/pair/common/schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import z from 'zod/v3'

/**
* Optional pipe id when linking a resource to a flow (must be a UUID when set).
*/
export const optionalFlowIdSchema = z.string().uuid().nullish()

export const outputFieldNameSchema = z
.string()
.min(1, { message: 'Output name is required' })
.max(64, {
message: 'Output name cannot be more than 64 characters',
})
.regex(/^[a-zA-Z0-9\s]+$/, {
message: 'Output name can only contain letters, numbers, and spaces',
})

/** Spaces in UI-validated output names become underscores for storage / API keys. */
export function sanitizeOutputFieldName(name: string): string {
return name.replace(/\s/g, '_')
}

/** After `outputFieldNameSchema` parses, map to sanitized key + user-entered original. */
export function toSanitizedOutputFieldNamePair(validatedName: string): {
fieldName: string
originalFieldName: string
} {
return {
fieldName: sanitizeOutputFieldName(validatedName),
originalFieldName: validatedName,
}
}
5 changes: 4 additions & 1 deletion packages/frontend/src/components/RichTextEditor/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,9 @@ const Editor = ({
const { allApps } = useContext(EditorContext)
const isMobile = useIsMobile()
const isMulticol = parentType === 'multicol'
const hasCustomMenuOptions =
!customRteMenuOptions || customRteMenuOptions.length > 0
const shouldShowMenuBar = isRich && hasCustomMenuOptions

// ref to track the defaultValue
// this is to sync the content of the editor with the defaultValue
Expand Down Expand Up @@ -321,7 +324,7 @@ const Editor = ({
>
<PopoverTrigger>
<Box className={isMulticol ? 'single-line-editor' : undefined}>
{isRich && (
{shouldShowMenuBar && (
<MenuBar
editor={editor}
variableMap={varInfo}
Expand Down
Loading