From e5edc8793e69fbb0307b30718d1b6c66412e30a9 Mon Sep 17 00:00:00 2001 From: Jack Nam Date: Thu, 26 Feb 2026 13:39:54 -0800 Subject: [PATCH 1/6] Add initial commit --- src/ONYXKEYS.ts | 5 + src/ROUTES.ts | 9 + src/SCREENS.ts | 2 + src/languages/en.ts | 39 ++++ src/languages/es.ts | 39 ++++ .../parameters/CreateVirtualEmployeeParams.ts | 9 + .../parameters/DeleteVirtualEmployeeParams.ts | 6 + ...OpenWorkspaceVirtualEmployeesPageParams.ts | 5 + .../parameters/UpdateVirtualEmployeeParams.ts | 10 + src/libs/API/parameters/index.ts | 4 + src/libs/API/types.ts | 8 + .../Navigators/WorkspaceSplitNavigator.tsx | 2 + .../RELATIONS/WORKSPACE_TO_RHP.ts | 1 + src/libs/Navigation/linkingConfig/config.ts | 6 + src/libs/Navigation/types.ts | 7 + src/libs/actions/VirtualEmployee.ts | 125 ++++++++++++ src/pages/workspace/WorkspaceInitialPage.tsx | 9 + .../WorkspaceVirtualEmployeePage.tsx | 192 ++++++++++++++++++ .../WorkspaceVirtualEmployeesPage.tsx | 157 ++++++++++++++ src/types/onyx/VirtualEmployee.ts | 35 ++++ src/types/onyx/index.ts | 4 + 21 files changed, 674 insertions(+) create mode 100644 src/libs/API/parameters/CreateVirtualEmployeeParams.ts create mode 100644 src/libs/API/parameters/DeleteVirtualEmployeeParams.ts create mode 100644 src/libs/API/parameters/OpenWorkspaceVirtualEmployeesPageParams.ts create mode 100644 src/libs/API/parameters/UpdateVirtualEmployeeParams.ts create mode 100644 src/libs/actions/VirtualEmployee.ts create mode 100644 src/pages/workspace/virtualEmployees/WorkspaceVirtualEmployeePage.tsx create mode 100644 src/pages/workspace/virtualEmployees/WorkspaceVirtualEmployeesPage.tsx create mode 100644 src/types/onyx/VirtualEmployee.ts diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 00ef647995498..ebb7ca55ee095 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -652,6 +652,9 @@ const ONYXKEYS = { /** The transaction IDs to be highlighted when opening the Expenses search route page */ TRANSACTION_IDS_HIGHLIGHT_ON_SEARCH_ROUTE: 'transactionIdsHighlightOnSearchRoute', + /** Virtual employees configured for a workspace, keyed by policyID */ + VIRTUAL_EMPLOYEES: 'virtualEmployees', + /** Collection Keys */ COLLECTION: { DOMAIN: 'domain_', @@ -777,6 +780,8 @@ const ONYXKEYS = { * Key format: passkey_${userId} */ PASSKEY_CREDENTIALS: 'passkeyCredentials_', + VIRTUAL_EMPLOYEES: 'virtualEmployees_', + VIRTUAL_EMPLOYEE_ACTIONS: 'virtualEmployeeActions_', }, /** List of Form ids */ diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 2f7de242fe33c..c61a3006c6d6d 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -2636,6 +2636,15 @@ const ROUTES = { route: 'workspaces/:policyID/travel/missing-personal-details', getRoute: (policyID: string) => `workspaces/${policyID}/travel/missing-personal-details` as const, }, + WORKSPACE_VIRTUAL_EMPLOYEES: { + route: 'workspace/:policyID/virtual-employees', + getRoute: (policyID: string) => `workspace/${policyID}/virtual-employees` as const, + }, + WORKSPACE_VIRTUAL_EMPLOYEES_EDIT: { + route: 'workspace/:policyID/virtual-employees/:virtualEmployeeID', + getRoute: (policyID: string, virtualEmployeeID: string) => + `workspace/${policyID}/virtual-employees/${virtualEmployeeID}` as const, + }, WORKSPACE_CREATE_DISTANCE_RATE: { route: 'workspaces/:policyID/distance-rates/new', getRoute: (policyID: string, transactionID?: string, reportID?: string) => diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 986beb3ac0a4c..8459a18969720 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -807,6 +807,8 @@ const SCREENS = { PER_DIEM_EDIT_CURRENCY: 'Per_Diem_Edit_Currency', TIME_TRACKING: 'Time_Tracking', TIME_TRACKING_DEFAULT_RATE: 'Time_Tracking_Default_Rate', + VIRTUAL_EMPLOYEES: 'Workspace_VirtualEmployees', + VIRTUAL_EMPLOYEES_EDIT: 'Workspace_VirtualEmployees_Edit', }, EDIT_REQUEST: { diff --git a/src/languages/en.ts b/src/languages/en.ts index 3c02f0097a8c3..5844e51339efe 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -5339,6 +5339,45 @@ const translations = { defaultHourlyRate: 'Default hourly rate', }, }, + virtualEmployees: { + title: 'Virtual employees', + addNew: 'Add virtual employee', + createTitle: 'Create virtual employee', + editTitle: 'Edit virtual employee', + emptyStateTitle: 'No virtual employees yet', + emptyStateDescription: 'Create AI-powered virtual employees to automate expense management tasks in your workspace.', + displayNameLabel: 'Display name', + systemPromptLabel: 'System prompt', + systemPromptHint: 'Describe the role and behavior of this virtual employee. Minimum 20 characters.', + capabilitiesSection: 'Capabilities', + eventsSection: 'Event subscriptions', + noCapabilities: 'No capabilities', + capabilityCount: ({count}: {count: number}) => (count === 1 ? '1 capability' : `${count} capabilities`), + create: 'Create', + capabilities: { + readTransactions: 'Read transactions', + editTransactions: 'Edit transactions', + sendMessages: 'Send messages', + approveReports: 'Approve reports', + rejectReports: 'Reject reports', + dismissViolations: 'Dismiss violations', + readPolicy: 'Read policy', + readReports: 'Read reports', + }, + events: { + transactionCreated: 'Transaction created', + transactionModified: 'Transaction modified', + receiptScanned: 'Receipt scanned', + reportSubmitted: 'Report submitted', + reportApproved: 'Report approved', + chatMention: 'Chat mention', + chatMessage: 'Chat message', + }, + errors: { + displayNameRequired: 'Display name is required.', + systemPromptMinLength: ({minLength}: {minLength: number}) => `System prompt must be at least ${minLength} characters.`, + }, + }, reports: { reportsCustomTitleExamples: 'Examples:', customReportNamesSubtitle: `Customize report titles using our extensive formulas.`, diff --git a/src/languages/es.ts b/src/languages/es.ts index 091cfe1e11a0a..c24570801a09f 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -5187,6 +5187,45 @@ ${amount} para ${merchant} - ${date}`, defaultHourlyRate: 'Tarifa por hora predeterminada', }, }, + virtualEmployees: { + title: 'Empleados virtuales', + addNew: 'Agregar empleado virtual', + createTitle: 'Crear empleado virtual', + editTitle: 'Editar empleado virtual', + emptyStateTitle: 'No hay empleados virtuales', + emptyStateDescription: 'Crea empleados virtuales con IA para automatizar tareas de gestión de gastos en tu espacio de trabajo.', + displayNameLabel: 'Nombre para mostrar', + systemPromptLabel: 'Prompt del sistema', + systemPromptHint: 'Describe el rol y comportamiento de este empleado virtual. Mínimo 20 caracteres.', + capabilitiesSection: 'Capacidades', + eventsSection: 'Suscripciones a eventos', + noCapabilities: 'Sin capacidades', + capabilityCount: ({count}: {count: number}) => (count === 1 ? '1 capacidad' : `${count} capacidades`), + create: 'Crear', + capabilities: { + readTransactions: 'Leer transacciones', + editTransactions: 'Editar transacciones', + sendMessages: 'Enviar mensajes', + approveReports: 'Aprobar informes', + rejectReports: 'Rechazar informes', + dismissViolations: 'Descartar violaciones', + readPolicy: 'Leer política', + readReports: 'Leer informes', + }, + events: { + transactionCreated: 'Transacción creada', + transactionModified: 'Transacción modificada', + receiptScanned: 'Recibo escaneado', + reportSubmitted: 'Informe enviado', + reportApproved: 'Informe aprobado', + chatMention: 'Mención en chat', + chatMessage: 'Mensaje de chat', + }, + errors: { + displayNameRequired: 'El nombre para mostrar es obligatorio.', + systemPromptMinLength: ({minLength}: {minLength: number}) => `El prompt del sistema debe tener al menos ${minLength} caracteres.`, + }, + }, reports: { reportsCustomTitleExamples: 'Ejemplos:', customReportNamesSubtitle: `Personaliza los títulos de los informes usando nuestras amplias fórmulas.`, diff --git a/src/libs/API/parameters/CreateVirtualEmployeeParams.ts b/src/libs/API/parameters/CreateVirtualEmployeeParams.ts new file mode 100644 index 0000000000000..190140f05fb90 --- /dev/null +++ b/src/libs/API/parameters/CreateVirtualEmployeeParams.ts @@ -0,0 +1,9 @@ +type CreateVirtualEmployeeParams = { + policyID: string; + displayName: string; + systemPrompt: string; + capabilities: string; + eventSubs: string; +}; + +export default CreateVirtualEmployeeParams; diff --git a/src/libs/API/parameters/DeleteVirtualEmployeeParams.ts b/src/libs/API/parameters/DeleteVirtualEmployeeParams.ts new file mode 100644 index 0000000000000..c83735cfdc44a --- /dev/null +++ b/src/libs/API/parameters/DeleteVirtualEmployeeParams.ts @@ -0,0 +1,6 @@ +type DeleteVirtualEmployeeParams = { + policyID: string; + vaAccountID: number; +}; + +export default DeleteVirtualEmployeeParams; diff --git a/src/libs/API/parameters/OpenWorkspaceVirtualEmployeesPageParams.ts b/src/libs/API/parameters/OpenWorkspaceVirtualEmployeesPageParams.ts new file mode 100644 index 0000000000000..6b4b75d30bfc1 --- /dev/null +++ b/src/libs/API/parameters/OpenWorkspaceVirtualEmployeesPageParams.ts @@ -0,0 +1,5 @@ +type OpenWorkspaceVirtualEmployeesPageParams = { + policyID: string; +}; + +export default OpenWorkspaceVirtualEmployeesPageParams; diff --git a/src/libs/API/parameters/UpdateVirtualEmployeeParams.ts b/src/libs/API/parameters/UpdateVirtualEmployeeParams.ts new file mode 100644 index 0000000000000..814746b64c0e4 --- /dev/null +++ b/src/libs/API/parameters/UpdateVirtualEmployeeParams.ts @@ -0,0 +1,10 @@ +type UpdateVirtualEmployeeParams = { + policyID: string; + vaAccountID: number; + displayName: string; + systemPrompt: string; + capabilities: string; + eventSubs: string; +}; + +export default UpdateVirtualEmployeeParams; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index 6f66f2af6a53b..f0765d992ea7e 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -489,3 +489,7 @@ export type {default as ToggleTwoFactorAuthRequiredForDomainParams} from './Togg export type {default as DeleteVacationDelegateParams} from './DeleteVacationDelegateParams'; export type {default as SetTwoFactorAuthExemptEmailForDomainParams} from './SetTwoFactorAuthExemptEmailForDomainParams'; export type {default as ResetDomainMemberTwoFactorAuthParams} from './ResetDomainMemberTwoFactorAuthParams'; +export type {default as CreateVirtualEmployeeParams} from './CreateVirtualEmployeeParams'; +export type {default as UpdateVirtualEmployeeParams} from './UpdateVirtualEmployeeParams'; +export type {default as DeleteVirtualEmployeeParams} from './DeleteVirtualEmployeeParams'; +export type {default as OpenWorkspaceVirtualEmployeesPageParams} from './OpenWorkspaceVirtualEmployeesPageParams'; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index f3f50e9481ebf..fd23f4fdd9c78 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -561,6 +561,9 @@ const WRITE_COMMANDS = { TOGGLE_TWO_FACTOR_AUTH_REQUIRED_FOR_DOMAIN: 'ToggleTwoFactorAuthRequiredForDomain', SET_TWO_FACTOR_AUTH_EXEMPT_EMAIL_FOR_DOMAIN: 'SetTwoFactorAuthExemptEmailForDomain', RESET_DOMAIN_MEMBER_TWO_FACTOR_AUTH: 'ResetDomainMemberTwoFactorAuth', + CREATE_VIRTUAL_EMPLOYEE: 'CreateVirtualEmployee', + UPDATE_VIRTUAL_EMPLOYEE: 'UpdateVirtualEmployee', + DELETE_VIRTUAL_EMPLOYEE: 'DeleteVirtualEmployee', } as const; type WriteCommand = ValueOf; @@ -1139,6 +1142,9 @@ type WriteCommandParameters = { [WRITE_COMMANDS.TOGGLE_TWO_FACTOR_AUTH_REQUIRED_FOR_DOMAIN]: Parameters.ToggleTwoFactorAuthRequiredForDomainParams; [WRITE_COMMANDS.SET_TWO_FACTOR_AUTH_EXEMPT_EMAIL_FOR_DOMAIN]: Parameters.SetTwoFactorAuthExemptEmailForDomainParams; [WRITE_COMMANDS.RESET_DOMAIN_MEMBER_TWO_FACTOR_AUTH]: Parameters.ResetDomainMemberTwoFactorAuthParams; + [WRITE_COMMANDS.CREATE_VIRTUAL_EMPLOYEE]: Parameters.CreateVirtualEmployeeParams; + [WRITE_COMMANDS.UPDATE_VIRTUAL_EMPLOYEE]: Parameters.UpdateVirtualEmployeeParams; + [WRITE_COMMANDS.DELETE_VIRTUAL_EMPLOYEE]: Parameters.DeleteVirtualEmployeeParams; }; const READ_COMMANDS = { @@ -1348,6 +1354,7 @@ const SIDE_EFFECT_REQUEST_COMMANDS = { TROUBLESHOOT_MULTIFACTOR_AUTHENTICATION: 'TroubleshootMultifactorAuthentication', REQUEST_AUTHENTICATION_CHALLENGE: 'RequestAuthenticationChallenge', REVOKE_MULTIFACTOR_AUTHENTICATION_CREDENTIALS: 'RevokeMultifactorAuthenticationCredentials', + OPEN_WORKSPACE_VIRTUAL_EMPLOYEES_PAGE: 'OpenWorkspaceVirtualEmployeesPage', } as const; type SideEffectRequestCommand = ValueOf; @@ -1380,6 +1387,7 @@ type SideEffectRequestCommandParameters = { [SIDE_EFFECT_REQUEST_COMMANDS.TROUBLESHOOT_MULTIFACTOR_AUTHENTICATION]: Parameters.TroubleshootMultifactorAuthenticationParams; [SIDE_EFFECT_REQUEST_COMMANDS.REQUEST_AUTHENTICATION_CHALLENGE]: Parameters.RequestAuthenticationChallengeParams; [SIDE_EFFECT_REQUEST_COMMANDS.REVOKE_MULTIFACTOR_AUTHENTICATION_CREDENTIALS]: EmptyObject; + [SIDE_EFFECT_REQUEST_COMMANDS.OPEN_WORKSPACE_VIRTUAL_EMPLOYEES_PAGE]: Parameters.OpenWorkspaceVirtualEmployeesPageParams; }; type ApiRequestCommandParameters = WriteCommandParameters & ReadCommandParameters & SideEffectRequestCommandParameters; diff --git a/src/libs/Navigation/AppNavigator/Navigators/WorkspaceSplitNavigator.tsx b/src/libs/Navigation/AppNavigator/Navigators/WorkspaceSplitNavigator.tsx index 2345747303a14..c6beac1ef048a 100644 --- a/src/libs/Navigation/AppNavigator/Navigators/WorkspaceSplitNavigator.tsx +++ b/src/libs/Navigation/AppNavigator/Navigators/WorkspaceSplitNavigator.tsx @@ -35,6 +35,8 @@ const CENTRAL_PANE_WORKSPACE_SCREENS = { [SCREENS.WORKSPACE.TRAVEL]: () => require('../../../../pages/workspace/travel/PolicyTravelPage').default, [SCREENS.WORKSPACE.RULES]: () => require('../../../../pages/workspace/rules/PolicyRulesPage').default, [SCREENS.WORKSPACE.TIME_TRACKING]: () => require('../../../../pages/workspace/timeTracking/WorkspaceTimeTrackingPage').default, + [SCREENS.WORKSPACE.VIRTUAL_EMPLOYEES]: () => require('../../../../pages/workspace/virtualEmployees/WorkspaceVirtualEmployeesPage').default, + [SCREENS.WORKSPACE.VIRTUAL_EMPLOYEES_EDIT]: () => require('../../../../pages/workspace/virtualEmployees/WorkspaceVirtualEmployeePage').default, } satisfies Screens; const Split = createSplitNavigator(); diff --git a/src/libs/Navigation/linkingConfig/RELATIONS/WORKSPACE_TO_RHP.ts b/src/libs/Navigation/linkingConfig/RELATIONS/WORKSPACE_TO_RHP.ts index d38863497639a..c5b94634e3a09 100755 --- a/src/libs/Navigation/linkingConfig/RELATIONS/WORKSPACE_TO_RHP.ts +++ b/src/libs/Navigation/linkingConfig/RELATIONS/WORKSPACE_TO_RHP.ts @@ -317,6 +317,7 @@ const WORKSPACE_TO_RHP: Partial['config'] = { [SCREENS.WORKSPACE.TIME_TRACKING]: { path: ROUTES.WORKSPACE_TIME_TRACKING.route, }, + [SCREENS.WORKSPACE.VIRTUAL_EMPLOYEES]: { + path: ROUTES.WORKSPACE_VIRTUAL_EMPLOYEES.route, + }, + [SCREENS.WORKSPACE.VIRTUAL_EMPLOYEES_EDIT]: { + path: ROUTES.WORKSPACE_VIRTUAL_EMPLOYEES_EDIT.route, + }, }, }, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 1ea09d1fc6ac3..8d7e14f485a83 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -2721,6 +2721,13 @@ type WorkspaceSplitNavigatorParamList = { [SCREENS.WORKSPACE.TIME_TRACKING]: { policyID: string; }; + [SCREENS.WORKSPACE.VIRTUAL_EMPLOYEES]: { + policyID: string; + }; + [SCREENS.WORKSPACE.VIRTUAL_EMPLOYEES_EDIT]: { + policyID: string; + virtualEmployeeID: string; + }; }; type DomainSplitNavigatorParamList = { diff --git a/src/libs/actions/VirtualEmployee.ts b/src/libs/actions/VirtualEmployee.ts new file mode 100644 index 0000000000000..e9863d4b34c83 --- /dev/null +++ b/src/libs/actions/VirtualEmployee.ts @@ -0,0 +1,125 @@ +import Onyx from 'react-native-onyx'; +import * as API from '@libs/API'; +import {SIDE_EFFECT_REQUEST_COMMANDS, WRITE_COMMANDS} from '@libs/API/types'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {VirtualEmployee, VirtualEmployeeCapability, VirtualEmployeeEventSubscription} from '@src/types/onyx/VirtualEmployee'; + +function createVirtualEmployee(policyID: string, displayName: string, systemPrompt: string, capabilities: VirtualEmployeeCapability[], eventSubs: VirtualEmployeeEventSubscription[]): void { + const optimisticID = `pending_${Date.now()}`; + const optimisticEmployee: VirtualEmployee = { + id: optimisticID, + policyID, + accountID: 0, + email: '', + displayName, + systemPrompt, + capabilities, + eventSubs, + status: 'active', + createdBy: 0, + pendingAction: 'add', + }; + + const optimisticData = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.VIRTUAL_EMPLOYEES}${policyID}`, + value: {[optimisticID]: optimisticEmployee}, + }, + ]; + + const successData = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.VIRTUAL_EMPLOYEES}${policyID}`, + value: {[optimisticID]: null}, + }, + ]; + + const failureData = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.VIRTUAL_EMPLOYEES}${policyID}`, + value: {[optimisticID]: {pendingAction: null, errors: {create: 'Failed to create virtual employee'}}}, + }, + ]; + + API.write( + WRITE_COMMANDS.CREATE_VIRTUAL_EMPLOYEE, + {policyID, displayName, systemPrompt, capabilities: JSON.stringify(capabilities), eventSubs: JSON.stringify(eventSubs)}, + {optimisticData, successData, failureData}, + ); +} + +function updateVirtualEmployee(policyID: string, virtualEmployeeID: string, updates: Partial>): void { + const optimisticData = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.VIRTUAL_EMPLOYEES}${policyID}`, + value: {[virtualEmployeeID]: {...updates, pendingAction: 'update'}}, + }, + ]; + + const successData = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.VIRTUAL_EMPLOYEES}${policyID}`, + value: {[virtualEmployeeID]: {pendingAction: null}}, + }, + ]; + + const failureData = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.VIRTUAL_EMPLOYEES}${policyID}`, + value: {[virtualEmployeeID]: {pendingAction: null, errors: {update: 'Failed to update'}}}, + }, + ]; + + API.write( + WRITE_COMMANDS.UPDATE_VIRTUAL_EMPLOYEE, + { + policyID, + vaAccountID: Number(virtualEmployeeID), + displayName: updates.displayName ?? '', + systemPrompt: updates.systemPrompt ?? '', + capabilities: JSON.stringify(updates.capabilities), + eventSubs: JSON.stringify(updates.eventSubs), + }, + {optimisticData, successData, failureData}, + ); +} + +function deleteVirtualEmployee(policyID: string, virtualEmployeeID: string): void { + const optimisticData = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.VIRTUAL_EMPLOYEES}${policyID}`, + value: {[virtualEmployeeID]: {pendingAction: 'delete'}}, + }, + ]; + + const successData = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.VIRTUAL_EMPLOYEES}${policyID}`, + value: {[virtualEmployeeID]: null}, + }, + ]; + + const failureData = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.VIRTUAL_EMPLOYEES}${policyID}`, + value: {[virtualEmployeeID]: {pendingAction: null, errors: {delete: 'Failed to delete'}}}, + }, + ]; + + API.write(WRITE_COMMANDS.DELETE_VIRTUAL_EMPLOYEE, {policyID, vaAccountID: Number(virtualEmployeeID)}, {optimisticData, successData, failureData}); +} + +function openWorkspaceVirtualEmployeesPage(policyID: string): void { + API.makeRequestWithSideEffects(SIDE_EFFECT_REQUEST_COMMANDS.OPEN_WORKSPACE_VIRTUAL_EMPLOYEES_PAGE, {policyID}, {}); +} + +export {createVirtualEmployee, updateVirtualEmployee, deleteVirtualEmployee, openWorkspaceVirtualEmployeesPage}; diff --git a/src/pages/workspace/WorkspaceInitialPage.tsx b/src/pages/workspace/WorkspaceInitialPage.tsx index 511900116112b..391eafe35156f 100644 --- a/src/pages/workspace/WorkspaceInitialPage.tsx +++ b/src/pages/workspace/WorkspaceInitialPage.tsx @@ -111,6 +111,7 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, route}: Workspac 'Workflows', 'LuggageWithLines', 'Clock', + 'Sparkles', ] as const); const policy = policyDraft?.id ? policyDraft : policyProp; @@ -384,6 +385,13 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, route}: Workspac }); } + protectedMenuItems.push({ + translationKey: 'workspace.virtualEmployees.title', + icon: expensifyIcons.Sparkles, + action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_VIRTUAL_EMPLOYEES.getRoute(policyID)))), + screenName: SCREENS.WORKSPACE.VIRTUAL_EMPLOYEES, + }); + protectedMenuItems.push({ translationKey: 'workspace.common.moreFeatures', icon: expensifyIcons.Gear, @@ -432,6 +440,7 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, route}: Workspac expensifyIcons.CalendarSolid, expensifyIcons.InvoiceGeneric, expensifyIcons.Clock, + expensifyIcons.Sparkles, singleExecution, waitForNavigate, featureStates, diff --git a/src/pages/workspace/virtualEmployees/WorkspaceVirtualEmployeePage.tsx b/src/pages/workspace/virtualEmployees/WorkspaceVirtualEmployeePage.tsx new file mode 100644 index 0000000000000..a4d0fa8115df3 --- /dev/null +++ b/src/pages/workspace/virtualEmployees/WorkspaceVirtualEmployeePage.tsx @@ -0,0 +1,192 @@ +import React, {useCallback, useMemo, useState} from 'react'; +import {View} from 'react-native'; +import Button from '@components/Button'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import ScreenWrapper from '@components/ScreenWrapper'; +import ScrollView from '@components/ScrollView'; +import Switch from '@components/Switch'; +import Text from '@components/Text'; +import TextInput from '@components/TextInput'; +import useLocalize from '@hooks/useLocalize'; +import useNetwork from '@hooks/useNetwork'; +import useOnyx from '@hooks/useOnyx'; +import useThemeStyles from '@hooks/useThemeStyles'; +import Navigation from '@libs/Navigation/Navigation'; +import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; +import type {WorkspaceSplitNavigatorParamList} from '@libs/Navigation/types'; +import type {TranslationPaths} from '@src/languages/types'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type SCREENS from '@src/SCREENS'; +import type {VirtualEmployee, VirtualEmployeeCapability, VirtualEmployeeEventSubscription} from '@src/types/onyx/VirtualEmployee'; + +type WorkspaceVirtualEmployeePageProps = PlatformStackScreenProps; + +const ALL_CAPABILITIES: Array<{key: VirtualEmployeeCapability; labelKey: TranslationPaths}> = [ + {key: 'can_read_transactions', labelKey: 'workspace.virtualEmployees.capabilities.readTransactions'}, + {key: 'can_edit_transactions', labelKey: 'workspace.virtualEmployees.capabilities.editTransactions'}, + {key: 'can_send_messages', labelKey: 'workspace.virtualEmployees.capabilities.sendMessages'}, + {key: 'can_approve_reports', labelKey: 'workspace.virtualEmployees.capabilities.approveReports'}, + {key: 'can_reject_reports', labelKey: 'workspace.virtualEmployees.capabilities.rejectReports'}, + {key: 'can_dismiss_violations', labelKey: 'workspace.virtualEmployees.capabilities.dismissViolations'}, + {key: 'can_read_policy', labelKey: 'workspace.virtualEmployees.capabilities.readPolicy'}, + {key: 'can_read_reports', labelKey: 'workspace.virtualEmployees.capabilities.readReports'}, +]; + +const ALL_EVENTS: Array<{key: VirtualEmployeeEventSubscription; labelKey: TranslationPaths}> = [ + {key: 'transaction.created', labelKey: 'workspace.virtualEmployees.events.transactionCreated'}, + {key: 'transaction.modified', labelKey: 'workspace.virtualEmployees.events.transactionModified'}, + {key: 'transaction.receipt_scanned', labelKey: 'workspace.virtualEmployees.events.receiptScanned'}, + {key: 'report.submitted', labelKey: 'workspace.virtualEmployees.events.reportSubmitted'}, + {key: 'report.approved', labelKey: 'workspace.virtualEmployees.events.reportApproved'}, + {key: 'chat.mention', labelKey: 'workspace.virtualEmployees.events.chatMention'}, + {key: 'chat.message', labelKey: 'workspace.virtualEmployees.events.chatMessage'}, +]; + +const MIN_SYSTEM_PROMPT_LENGTH = 20; + +function WorkspaceVirtualEmployeePage({route}: WorkspaceVirtualEmployeePageProps) { + const {policyID, virtualEmployeeID} = route.params; + const isNew = virtualEmployeeID === 'new'; + const styles = useThemeStyles(); + const {translate} = useLocalize(); + const {isOffline} = useNetwork(); + + const [virtualEmployeesCollection] = useOnyx(`${ONYXKEYS.COLLECTION.VIRTUAL_EMPLOYEES}${policyID}`); + + const existingVE: VirtualEmployee | undefined = useMemo(() => { + if (isNew || !virtualEmployeesCollection) { + return undefined; + } + return Object.values(virtualEmployeesCollection).find((ve): ve is VirtualEmployee => !!ve && ve.id === virtualEmployeeID); + }, [isNew, virtualEmployeesCollection, virtualEmployeeID]); + + const [displayName, setDisplayName] = useState(existingVE?.displayName ?? ''); + const [systemPrompt, setSystemPrompt] = useState(existingVE?.systemPrompt ?? ''); + const [capabilities, setCapabilities] = useState(existingVE?.capabilities ?? []); + const [eventSubs, setEventSubs] = useState(existingVE?.eventSubs ?? []); + + const [displayNameError, setDisplayNameError] = useState(''); + const [systemPromptError, setSystemPromptError] = useState(''); + + const toggleCapability = useCallback((cap: VirtualEmployeeCapability) => { + setCapabilities((prev) => (prev.includes(cap) ? prev.filter((c) => c !== cap) : [...prev, cap])); + }, []); + + const toggleEvent = useCallback((event: VirtualEmployeeEventSubscription) => { + setEventSubs((prev) => (prev.includes(event) ? prev.filter((e) => e !== event) : [...prev, event])); + }, []); + + const validate = useCallback((): boolean => { + let isValid = true; + + if (!displayName.trim()) { + setDisplayNameError(translate('workspace.virtualEmployees.errors.displayNameRequired')); + isValid = false; + } else { + setDisplayNameError(''); + } + + if (systemPrompt.trim().length < MIN_SYSTEM_PROMPT_LENGTH) { + setSystemPromptError(translate('workspace.virtualEmployees.errors.systemPromptMinLength', {minLength: MIN_SYSTEM_PROMPT_LENGTH})); + isValid = false; + } else { + setSystemPromptError(''); + } + + return isValid; + }, [displayName, systemPrompt, translate]); + + const handleSave = useCallback(() => { + if (!validate()) { + return; + } + + // TODO: Wire up to createVirtualEmployee / updateVirtualEmployee actions when available + // const params = { + // policyID, + // displayName: displayName.trim(), + // systemPrompt: systemPrompt.trim(), + // capabilities, + // eventSubs, + // }; + + Navigation.goBack(); + }, [validate]); + + return ( + + + + + + + + + + + {translate('workspace.virtualEmployees.systemPromptHint')} + + + + {translate('workspace.virtualEmployees.capabilitiesSection')} + {ALL_CAPABILITIES.map(({key, labelKey}) => ( + + {translate(labelKey)} + toggleCapability(key)} + /> + + ))} + + {translate('workspace.virtualEmployees.eventsSection')} + {ALL_EVENTS.map(({key, labelKey}) => ( + + {translate(labelKey)} + toggleEvent(key)} + /> + + ))} + + + + + ))} + + )} + + ); +} + +WorkspaceVirtualEmployeesPage.displayName = 'WorkspaceVirtualEmployeesPage'; + +export default WorkspaceVirtualEmployeesPage; diff --git a/src/types/onyx/VirtualEmployee.ts b/src/types/onyx/VirtualEmployee.ts new file mode 100644 index 0000000000000..ab3bee5f701b4 --- /dev/null +++ b/src/types/onyx/VirtualEmployee.ts @@ -0,0 +1,35 @@ +type VirtualEmployeeCapability = + | 'can_read_transactions' + | 'can_edit_transactions' + | 'can_send_messages' + | 'can_approve_reports' + | 'can_reject_reports' + | 'can_dismiss_violations' + | 'can_read_policy' + | 'can_read_reports'; + +type VirtualEmployeeEventSubscription = + | 'transaction.created' + | 'transaction.modified' + | 'transaction.receipt_scanned' + | 'report.submitted' + | 'report.approved' + | 'chat.mention' + | 'chat.message'; + +type VirtualEmployee = { + id: string; + policyID: string; + accountID: number; + email: string; + displayName: string; + systemPrompt: string; + capabilities: VirtualEmployeeCapability[]; + eventSubs: VirtualEmployeeEventSubscription[]; + status: 'active' | 'paused' | 'deleted'; + createdBy: number; + pendingAction?: 'add' | 'update' | 'delete'; + errors?: Record; +}; + +export type {VirtualEmployee, VirtualEmployeeCapability, VirtualEmployeeEventSubscription}; diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts index f86ee284b3679..d86ead9184d51 100644 --- a/src/types/onyx/index.ts +++ b/src/types/onyx/index.ts @@ -165,6 +165,7 @@ import type VacationDelegate from './VacationDelegate'; import type ValidateDomainTwoFactorCode from './ValidateDomainTwoFactorCode'; import type ValidateMagicCodeAction from './ValidateMagicCodeAction'; import type ValidateUserAndGetAccessiblePolicies from './ValidateUserAndGetAccessiblePolicies'; +import type {VirtualEmployee, VirtualEmployeeCapability, VirtualEmployeeEventSubscription} from './VirtualEmployee'; import type WalletAdditionalDetails from './WalletAdditionalDetails'; import type {WalletAdditionalQuestionDetails} from './WalletAdditionalDetails'; import type WalletOnfido from './WalletOnfido'; @@ -367,4 +368,7 @@ export type { CodingRuleMatchingTransaction, UserSecurityGroupData, DeviceBiometrics, + VirtualEmployee, + VirtualEmployeeCapability, + VirtualEmployeeEventSubscription, }; From 317b3679ad46357966a6ac3589cf68c87e4aeb7a Mon Sep 17 00:00:00 2001 From: Jack Nam Date: Thu, 26 Feb 2026 13:53:27 -0800 Subject: [PATCH 2/6] Improve UI --- .../WorkspaceVirtualEmployeePage.tsx | 170 ++++++++++++++---- .../WorkspaceVirtualEmployeesPage.tsx | 83 +++++---- 2 files changed, 183 insertions(+), 70 deletions(-) diff --git a/src/pages/workspace/virtualEmployees/WorkspaceVirtualEmployeePage.tsx b/src/pages/workspace/virtualEmployees/WorkspaceVirtualEmployeePage.tsx index a4d0fa8115df3..de4c69d35373a 100644 --- a/src/pages/workspace/virtualEmployees/WorkspaceVirtualEmployeePage.tsx +++ b/src/pages/workspace/virtualEmployees/WorkspaceVirtualEmployeePage.tsx @@ -11,6 +11,7 @@ import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useOnyx from '@hooks/useOnyx'; import useThemeStyles from '@hooks/useThemeStyles'; +import {createVirtualEmployee, updateVirtualEmployee} from '@libs/actions/VirtualEmployee'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {WorkspaceSplitNavigatorParamList} from '@libs/Navigation/types'; @@ -21,25 +22,97 @@ import type {VirtualEmployee, VirtualEmployeeCapability, VirtualEmployeeEventSub type WorkspaceVirtualEmployeePageProps = PlatformStackScreenProps; -const ALL_CAPABILITIES: Array<{key: VirtualEmployeeCapability; labelKey: TranslationPaths}> = [ - {key: 'can_read_transactions', labelKey: 'workspace.virtualEmployees.capabilities.readTransactions'}, - {key: 'can_edit_transactions', labelKey: 'workspace.virtualEmployees.capabilities.editTransactions'}, - {key: 'can_send_messages', labelKey: 'workspace.virtualEmployees.capabilities.sendMessages'}, - {key: 'can_approve_reports', labelKey: 'workspace.virtualEmployees.capabilities.approveReports'}, - {key: 'can_reject_reports', labelKey: 'workspace.virtualEmployees.capabilities.rejectReports'}, - {key: 'can_dismiss_violations', labelKey: 'workspace.virtualEmployees.capabilities.dismissViolations'}, - {key: 'can_read_policy', labelKey: 'workspace.virtualEmployees.capabilities.readPolicy'}, - {key: 'can_read_reports', labelKey: 'workspace.virtualEmployees.capabilities.readReports'}, +type CapabilityConfig = { + key: VirtualEmployeeCapability; + labelKey: TranslationPaths; + descriptionKey: TranslationPaths; +}; + +type EventConfig = { + key: VirtualEmployeeEventSubscription; + labelKey: TranslationPaths; + descriptionKey: TranslationPaths; +}; + +const ALL_CAPABILITIES: CapabilityConfig[] = [ + { + key: 'can_read_transactions', + labelKey: 'workspace.virtualEmployees.capabilities.readTransactions', + descriptionKey: 'workspace.virtualEmployees.capabilities.readTransactionsDescription', + }, + { + key: 'can_read_reports', + labelKey: 'workspace.virtualEmployees.capabilities.readReports', + descriptionKey: 'workspace.virtualEmployees.capabilities.readReportsDescription', + }, + { + key: 'can_edit_transactions', + labelKey: 'workspace.virtualEmployees.capabilities.editTransactions', + descriptionKey: 'workspace.virtualEmployees.capabilities.editTransactionsDescription', + }, + { + key: 'can_send_messages', + labelKey: 'workspace.virtualEmployees.capabilities.sendMessages', + descriptionKey: 'workspace.virtualEmployees.capabilities.sendMessagesDescription', + }, + { + key: 'can_approve_reports', + labelKey: 'workspace.virtualEmployees.capabilities.approveReports', + descriptionKey: 'workspace.virtualEmployees.capabilities.approveReportsDescription', + }, + { + key: 'can_reject_reports', + labelKey: 'workspace.virtualEmployees.capabilities.rejectReports', + descriptionKey: 'workspace.virtualEmployees.capabilities.rejectReportsDescription', + }, + { + key: 'can_dismiss_violations', + labelKey: 'workspace.virtualEmployees.capabilities.dismissViolations', + descriptionKey: 'workspace.virtualEmployees.capabilities.dismissViolationsDescription', + }, + { + key: 'can_read_policy', + labelKey: 'workspace.virtualEmployees.capabilities.readPolicy', + descriptionKey: 'workspace.virtualEmployees.capabilities.readPolicyDescription', + }, ]; -const ALL_EVENTS: Array<{key: VirtualEmployeeEventSubscription; labelKey: TranslationPaths}> = [ - {key: 'transaction.created', labelKey: 'workspace.virtualEmployees.events.transactionCreated'}, - {key: 'transaction.modified', labelKey: 'workspace.virtualEmployees.events.transactionModified'}, - {key: 'transaction.receipt_scanned', labelKey: 'workspace.virtualEmployees.events.receiptScanned'}, - {key: 'report.submitted', labelKey: 'workspace.virtualEmployees.events.reportSubmitted'}, - {key: 'report.approved', labelKey: 'workspace.virtualEmployees.events.reportApproved'}, - {key: 'chat.mention', labelKey: 'workspace.virtualEmployees.events.chatMention'}, - {key: 'chat.message', labelKey: 'workspace.virtualEmployees.events.chatMessage'}, +const ALL_EVENTS: EventConfig[] = [ + { + key: 'transaction.created', + labelKey: 'workspace.virtualEmployees.events.transactionCreated', + descriptionKey: 'workspace.virtualEmployees.events.transactionCreatedDescription', + }, + { + key: 'transaction.modified', + labelKey: 'workspace.virtualEmployees.events.transactionModified', + descriptionKey: 'workspace.virtualEmployees.events.transactionModifiedDescription', + }, + { + key: 'transaction.receipt_scanned', + labelKey: 'workspace.virtualEmployees.events.receiptScanned', + descriptionKey: 'workspace.virtualEmployees.events.receiptScannedDescription', + }, + { + key: 'report.submitted', + labelKey: 'workspace.virtualEmployees.events.reportSubmitted', + descriptionKey: 'workspace.virtualEmployees.events.reportSubmittedDescription', + }, + { + key: 'report.approved', + labelKey: 'workspace.virtualEmployees.events.reportApproved', + descriptionKey: 'workspace.virtualEmployees.events.reportApprovedDescription', + }, + { + key: 'chat.mention', + labelKey: 'workspace.virtualEmployees.events.chatMention', + descriptionKey: 'workspace.virtualEmployees.events.chatMentionDescription', + }, + { + key: 'chat.message', + labelKey: 'workspace.virtualEmployees.events.chatMessage', + descriptionKey: 'workspace.virtualEmployees.events.chatMessageDescription', + }, ]; const MIN_SYSTEM_PROMPT_LENGTH = 20; @@ -101,17 +174,19 @@ function WorkspaceVirtualEmployeePage({route}: WorkspaceVirtualEmployeePageProps return; } - // TODO: Wire up to createVirtualEmployee / updateVirtualEmployee actions when available - // const params = { - // policyID, - // displayName: displayName.trim(), - // systemPrompt: systemPrompt.trim(), - // capabilities, - // eventSubs, - // }; + if (isNew) { + createVirtualEmployee(policyID, displayName.trim(), systemPrompt.trim(), capabilities, eventSubs); + } else { + updateVirtualEmployee(policyID, virtualEmployeeID, { + displayName: displayName.trim(), + systemPrompt: systemPrompt.trim(), + capabilities, + eventSubs, + }); + } Navigation.goBack(); - }, [validate]); + }, [validate, isNew, policyID, displayName, systemPrompt, capabilities, eventSubs, virtualEmployeeID]); return ( @@ -120,6 +195,8 @@ function WorkspaceVirtualEmployeePage({route}: WorkspaceVirtualEmployeePageProps onBackButtonPress={Navigation.goBack} /> + + {/* Name */} + {/* System prompt */} @@ -145,34 +223,55 @@ function WorkspaceVirtualEmployeePage({route}: WorkspaceVirtualEmployeePageProps - {translate('workspace.virtualEmployees.capabilitiesSection')} - {ALL_CAPABILITIES.map(({key, labelKey}) => ( + {/* Divider */} + + + {/* Capabilities */} + {translate('workspace.virtualEmployees.capabilitiesSection')} + {translate('workspace.virtualEmployees.capabilitiesSectionHint')} + + {ALL_CAPABILITIES.map(({key, labelKey, descriptionKey}) => ( - {translate(labelKey)} + + {translate(labelKey)} + {translate(descriptionKey)} + toggleCapability(key)} + accessibilityLabel={translate(labelKey)} /> ))} - {translate('workspace.virtualEmployees.eventsSection')} - {ALL_EVENTS.map(({key, labelKey}) => ( + {/* Divider */} + + + {/* Event subscriptions */} + {translate('workspace.virtualEmployees.eventsSection')} + {translate('workspace.virtualEmployees.eventsSectionHint')} + + {ALL_EVENTS.map(({key, labelKey, descriptionKey}) => ( - {translate(labelKey)} + + {translate(labelKey)} + {translate(descriptionKey)} + toggleEvent(key)} + accessibilityLabel={translate(labelKey)} /> ))} + {/* Save */} - - ))} + navigateToEdit(ve.id)} + shouldShowRightIcon + badgeText={ve.status !== 'active' ? translate('workspace.virtualEmployees.statusPaused') : undefined} + /> + + ))} + )} From 767dcaa381c49d2e6fbb266dbf156222dba9bb58 Mon Sep 17 00:00:00 2001 From: Jack Nam Date: Thu, 26 Feb 2026 16:57:06 -0800 Subject: [PATCH 5/6] Improve styling --- src/languages/en.ts | 4 + src/languages/es.ts | 4 + .../WorkspaceVirtualEmployeePage.tsx | 256 ++++++++++++------ .../WorkspaceVirtualEmployeesPage.tsx | 10 +- 4 files changed, 185 insertions(+), 89 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index ba11cba8c5b70..58ae4067aaf23 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -5352,6 +5352,9 @@ const translations = { systemPromptPlaceholder: 'Example: You are an AP specialist for Acme Corp. When a report is submitted, check that every Amazon expense has a business purpose in the comment. If not, message the submitter and ask them to add one.', systemPromptHint: 'Be specific about what to check and when to act. The more detail, the better.', + unnamedEmployee: 'Unnamed employee', + virtualEmployeeLabel: 'Virtual Employee', + previewPlaceholder: 'Start typing instructions to preview how your virtual employee will behave\u2026', capabilitiesSection: 'Capabilities', capabilitiesSectionHint: 'Choose what actions this virtual employee is allowed to perform.', eventsSection: 'Triggers', @@ -5359,6 +5362,7 @@ const translations = { statusPaused: 'Paused', noCapabilities: 'No capabilities', capabilityCount: ({count}: {count: number}) => (count === 1 ? '1 capability' : `${count} capabilities`), + triggerCount: ({count}: {count: number}) => (count === 1 ? '1 trigger' : `${count} triggers`), create: 'Create', capabilities: { readTransactions: 'Read transactions', diff --git a/src/languages/es.ts b/src/languages/es.ts index c24570801a09f..e5642009a074e 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -5197,10 +5197,14 @@ ${amount} para ${merchant} - ${date}`, displayNameLabel: 'Nombre para mostrar', systemPromptLabel: 'Prompt del sistema', systemPromptHint: 'Describe el rol y comportamiento de este empleado virtual. Mínimo 20 caracteres.', + unnamedEmployee: 'Empleado sin nombre', + virtualEmployeeLabel: 'Empleado Virtual', + previewPlaceholder: 'Escribe instrucciones para previsualizar el comportamiento de tu empleado virtual\u2026', capabilitiesSection: 'Capacidades', eventsSection: 'Suscripciones a eventos', noCapabilities: 'Sin capacidades', capabilityCount: ({count}: {count: number}) => (count === 1 ? '1 capacidad' : `${count} capacidades`), + triggerCount: ({count}: {count: number}) => (count === 1 ? '1 activador' : `${count} activadores`), create: 'Crear', capabilities: { readTransactions: 'Leer transacciones', diff --git a/src/pages/workspace/virtualEmployees/WorkspaceVirtualEmployeePage.tsx b/src/pages/workspace/virtualEmployees/WorkspaceVirtualEmployeePage.tsx index dcd268a95d265..21f0c47752bc2 100644 --- a/src/pages/workspace/virtualEmployees/WorkspaceVirtualEmployeePage.tsx +++ b/src/pages/workspace/virtualEmployees/WorkspaceVirtualEmployeePage.tsx @@ -2,6 +2,7 @@ import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import {View} from 'react-native'; import Button from '@components/Button'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import Icon from '@components/Icon'; import ScreenWrapper from '@components/ScreenWrapper'; import ScrollView from '@components/ScrollView'; import Text from '@components/Text'; @@ -10,6 +11,7 @@ import {useMemoizedLazyIllustrations} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useOnyx from '@hooks/useOnyx'; +import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import {createVirtualEmployee, updateVirtualEmployee} from '@libs/actions/VirtualEmployee'; @@ -151,6 +153,7 @@ function WorkspaceVirtualEmployeePage({route}: WorkspaceVirtualEmployeePageProps const theme = useTheme(); const {translate} = useLocalize(); const {isOffline} = useNetwork(); + const {shouldUseNarrowLayout} = useResponsiveLayout(); const illustrations = useMemoizedLazyIllustrations([ 'ConciergeBot', @@ -249,8 +252,6 @@ function WorkspaceVirtualEmployeePage({route}: WorkspaceVirtualEmployeePageProps onBackButtonPress={Navigation.goBack} /> - - {/* Name — simple and prominent at the top */} - {/* Instructions — left-border block gives the prompt a distinct, intentional feel */} - - - + + - {translate('workspace.virtualEmployees.systemPromptLabel').toUpperCase()} + + {translate('workspace.virtualEmployees.systemPromptLabel').toUpperCase()} + + + + {!!systemPromptError && ( + + {systemPromptError} + + )} + + {translate('workspace.virtualEmployees.systemPromptHint')} - - {!!systemPromptError && ( - - {systemPromptError} - + + {!shouldUseNarrowLayout && ( + + + + + + + {displayName || translate('workspace.virtualEmployees.unnamedEmployee')} + + + {translate('workspace.virtualEmployees.virtualEmployeeLabel')} + + + + + {systemPrompt + ? `\u201C${systemPrompt.substring(0, 200)}${systemPrompt.length > 200 ? '\u2026' : ''}\u201D` + : translate('workspace.virtualEmployees.previewPlaceholder')} + + + + + + 0 ? theme.success : theme.icon, + marginRight: 6, + }} + /> + + {translate('workspace.virtualEmployees.capabilityCount', {count: capabilities.length})} + + + + 0 ? theme.success : theme.icon, + marginRight: 6, + }} + /> + + {translate('workspace.virtualEmployees.triggerCount', {count: eventSubs.length})} + + + + + )} - - {translate('workspace.virtualEmployees.systemPromptHint')} - - {/* Capabilities */} - - - - {translate('workspace.virtualEmployees.capabilitiesSection')} - - - {translate('workspace.virtualEmployees.capabilitiesSectionHint')} - + + + + + {translate('workspace.virtualEmployees.capabilitiesSection')} + + + {translate('workspace.virtualEmployees.capabilitiesSectionHint')} + + + {ALL_CAPABILITIES.map(({key, illustrationKey, labelKey, descriptionKey}) => ( + toggleCapability(key)} + wrapperStyle={[styles.ph5, styles.pv3]} + shouldPlaceSubtitleBelowSwitch + /> + ))} - {ALL_CAPABILITIES.map(({key, illustrationKey, labelKey, descriptionKey}) => ( - toggleCapability(key)} - wrapperStyle={[styles.ph5, styles.pv3]} - shouldPlaceSubtitleBelowSwitch - /> - ))} - - {/* Triggers */} - - - - {translate('workspace.virtualEmployees.eventsSection')} - - - {translate('workspace.virtualEmployees.eventsSectionHint')} - + + + + {translate('workspace.virtualEmployees.eventsSection')} + + + {translate('workspace.virtualEmployees.eventsSectionHint')} + + + {ALL_EVENTS.map(({key, illustrationKey, labelKey, descriptionKey}) => ( + toggleEvent(key)} + wrapperStyle={[styles.ph5, styles.pv3]} + shouldPlaceSubtitleBelowSwitch + /> + ))} - {ALL_EVENTS.map(({key, illustrationKey, labelKey, descriptionKey}) => ( - toggleEvent(key)} - wrapperStyle={[styles.ph5, styles.pv3]} - shouldPlaceSubtitleBelowSwitch - /> - ))} - {/* Save */}