feat(ai): add code interpreter for AI data analysis (#16559)

## Summary

- Add code interpreter tool that enables AI to execute Python code for
data analysis, CSV processing, and chart generation
- Support for both local (development) and E2B (sandboxed production)
execution drivers
- Real-time streaming of stdout/stderr and generated files
- Frontend components for displaying code execution results with
expandable sections

## Code Quality Improvements

- Extract `getMimeType` to shared utility to reduce code duplication
between drivers
- Fix security issue: escape single quotes/backslashes in E2B driver env
variable injection
- Add `buildExecutionState` helper to reduce duplicated state object
construction
- Add `DEFAULT_CODE_INTERPRETER_TIMEOUT_MS` constant for consistency
- Fix lingui linting warning and TypeScript theme errors in frontend

## Test Plan

- [ ] Test code interpreter with local driver in development
- [ ] Test code interpreter with E2B driver in production environment
- [ ] Verify streaming output displays correctly in chat UI
- [ ] Verify generated files (charts, CSVs) are uploaded and
downloadable
- [ ] Test file upload flow (CSV, Excel) triggers code interpreter

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> Updates generated i18n catalogs for Polish and pseudo-English, adding
strings for code execution/output (code interpreter) and various UI
messages, with minor text adjustments.
> 
> - **Localization**:
> - **Generated catalogs**: Refresh `locales/generated/pl-PL.ts` and
`locales/generated/pseudo-en.ts`.
> - Add strings for code execution/output (e.g., code, copy code/output,
running/waiting states, download files, generated files, Python code
execution).
> - Include new UI texts (errors, prompts, menus) and minor text
corrections.
>   - No changes to `pt-BR`; other files unchanged functionally.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
befc13d02c. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
This commit is contained in:
Félix Malfait 2025-12-15 16:11:24 +01:00 committed by GitHub
parent 4281a71f40
commit 2e104c8e76
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
151 changed files with 10361 additions and 449 deletions

View file

@ -624,6 +624,7 @@ export enum ConfigVariablesGroup {
BILLING_CONFIG = 'BILLING_CONFIG',
CAPTCHA_CONFIG = 'CAPTCHA_CONFIG',
CLOUDFLARE_CONFIG = 'CLOUDFLARE_CONFIG',
CODE_INTERPRETER_CONFIG = 'CODE_INTERPRETER_CONFIG',
EMAIL_SETTINGS = 'EMAIL_SETTINGS',
EXCEPTION_HANDLER = 'EXCEPTION_HANDLER',
GOOGLE_AUTH = 'GOOGLE_AUTH',
@ -3151,6 +3152,7 @@ export enum PermissionFlagType {
API_KEYS_AND_WEBHOOKS = 'API_KEYS_AND_WEBHOOKS',
APPLICATIONS = 'APPLICATIONS',
BILLING = 'BILLING',
CODE_INTERPRETER_TOOL = 'CODE_INTERPRETER_TOOL',
CONNECTED_ACCOUNTS = 'CONNECTED_ACCOUNTS',
DATA_MODEL = 'DATA_MODEL',
DOWNLOAD_FILE = 'DOWNLOAD_FILE',

View file

@ -624,6 +624,7 @@ export enum ConfigVariablesGroup {
BILLING_CONFIG = 'BILLING_CONFIG',
CAPTCHA_CONFIG = 'CAPTCHA_CONFIG',
CLOUDFLARE_CONFIG = 'CLOUDFLARE_CONFIG',
CODE_INTERPRETER_CONFIG = 'CODE_INTERPRETER_CONFIG',
EMAIL_SETTINGS = 'EMAIL_SETTINGS',
EXCEPTION_HANDLER = 'EXCEPTION_HANDLER',
GOOGLE_AUTH = 'GOOGLE_AUTH',
@ -3072,6 +3073,7 @@ export enum PermissionFlagType {
API_KEYS_AND_WEBHOOKS = 'API_KEYS_AND_WEBHOOKS',
APPLICATIONS = 'APPLICATIONS',
BILLING = 'BILLING',
CODE_INTERPRETER_TOOL = 'CODE_INTERPRETER_TOOL',
CONNECTED_ACCOUNTS = 'CONNECTED_ACCOUNTS',
DATA_MODEL = 'DATA_MODEL',
DOWNLOAD_FILE = 'DOWNLOAD_FILE',

View file

@ -2070,6 +2070,11 @@ msgstr "Maak toe"
msgid "Close command menu"
msgstr "Sluit opdragkieslys"
#. js-lingui-id: EWPtMO
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Code"
msgstr ""
#. js-lingui-id: /8PmQ9
#: src/modules/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowEditActionServerlessFunction.tsx
msgid "Code Editor"
@ -2147,6 +2152,7 @@ msgstr "voltooi"
#. js-lingui-id: qqWcBV
#: src/modules/settings/admin-panel/health-status/components/SettingsAdminQueueJobsTable.tsx
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Completed"
msgstr "Voltooi"
@ -2397,6 +2403,11 @@ msgstr "Gekopieer na knipbord"
msgid "Copy"
msgstr "Kopieer"
#. js-lingui-id: NmPNJJ
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Copy code"
msgstr ""
#. js-lingui-id: 7eVkEH
#: src/pages/onboarding/InviteTeam.tsx
msgid "Copy invitation link"
@ -2412,6 +2423,11 @@ msgstr "Kopieer skakel"
msgid "Copy link to view"
msgstr "Kopieer skakel na aansig"
#. js-lingui-id: VBIlvI
#: src/modules/ai/components/TerminalOutput.tsx
msgid "Copy output"
msgstr ""
#. js-lingui-id: HvB+os
#: src/pages/settings/SettingsTwoFactorAuthenticationMethod.tsx
msgid "Copy paste the code below"
@ -3452,6 +3468,11 @@ msgstr "Punte en komma - {dotsAndCommaExample}"
msgid "Downgrade"
msgstr "Verlaagde gradering"
#. js-lingui-id: qnTwse
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Download {filename}"
msgstr ""
#. js-lingui-id: WcWS//
#: src/modules/settings/security/components/SSO/SettingsSSOSAMLForm.tsx
msgid "Download file"
@ -4352,6 +4373,7 @@ msgstr "Ekstra Krediete Gebruik"
#. js-lingui-id: 7Bj3x9
#: src/pages/settings/emailing-domains/utils/getEmailingDomainStatusText.ts
#: src/modules/settings/admin-panel/health-status/components/SettingsAdminQueueJobsTable.tsx
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Failed"
msgstr "Misluk"
@ -4714,6 +4736,11 @@ msgstr "Algemeen"
msgid "General - Settings"
msgstr "Algemeen - Instellings"
#. js-lingui-id: wu9eMO
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Generated Files"
msgstr ""
#. js-lingui-id: DDcvSo
#: src/pages/settings/profile/appearance/components/LocalePicker.tsx
msgid "German"
@ -6670,6 +6697,11 @@ msgstr "Geen modelle beskikbaar nie. Stel asseblief KI-modelle in jou werkruimte
msgid "No option found"
msgstr "Geen opsie gevind nie"
#. js-lingui-id: XRc1G9
#: src/modules/ai/components/TerminalOutput.tsx
msgid "No output"
msgstr ""
#. js-lingui-id: tTItk7
#: src/modules/workflow/workflow-steps/components/WorkflowRunStepOutputDetail.tsx
msgid "No output available"
@ -7140,6 +7172,12 @@ msgstr "Ander werksruimtes"
msgid "Our team can help you set up your workspace to match your specific needs and workflows."
msgstr "Ons span kan jou help om jou werkruimte op te stel sodat dit by jou spesifieke behoeftes en werksvloei pas."
#. js-lingui-id: gh06VD
#: src/modules/ai/components/TerminalOutput.tsx
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Output"
msgstr ""
#. js-lingui-id: xvfn7l
#: src/modules/workflow/workflow-steps/workflow-actions/ai-agent-action/components/WorkflowOutputSchemaBuilder.tsx
msgid "Output Field {fieldNumber}"
@ -7585,6 +7623,11 @@ msgstr "Publieke domein suksesvol verwyder"
msgid "Public Domains"
msgstr "Publieke Domeine"
#. js-lingui-id: GUpZQM
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Python Code Execution"
msgstr ""
#. js-lingui-id: 2vudsu
#: src/modules/command-menu/pages/page-layout/utils/getDateGranularityLabel.ts
msgid "Quarter"
@ -8029,6 +8072,11 @@ msgstr "Voer 'n werkstroom uit en keer hier terug om die uitvoerings te sien"
msgid "Run Function"
msgstr "Voer Funksie Uit"
#. js-lingui-id: LTC198
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Running..."
msgstr ""
#. js-lingui-id: nji0/X
#: src/pages/settings/profile/appearance/components/LocalePicker.tsx
msgid "Russian"
@ -10192,6 +10240,11 @@ msgstr "Wag"
msgid "Waiting Children"
msgstr "Wagende Kinders"
#. js-lingui-id: TdQd9Q
#: src/modules/ai/components/TerminalOutput.tsx
msgid "Waiting for output..."
msgstr ""
#. js-lingui-id: zFSQY3
#: src/modules/activities/timeline-activities/rows/main-object/components/EventRowMainObject.tsx
msgid "was created by"

View file

@ -2070,6 +2070,11 @@ msgstr "إغلاق"
msgid "Close command menu"
msgstr "إغلاق قائمة الأوامر"
#. js-lingui-id: EWPtMO
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Code"
msgstr ""
#. js-lingui-id: /8PmQ9
#: src/modules/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowEditActionServerlessFunction.tsx
msgid "Code Editor"
@ -2147,6 +2152,7 @@ msgstr "مكتمل"
#. js-lingui-id: qqWcBV
#: src/modules/settings/admin-panel/health-status/components/SettingsAdminQueueJobsTable.tsx
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Completed"
msgstr "مكتمل"
@ -2397,6 +2403,11 @@ msgstr "تم النسخ إلى الحافظة"
msgid "Copy"
msgstr "نسخ"
#. js-lingui-id: NmPNJJ
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Copy code"
msgstr ""
#. js-lingui-id: 7eVkEH
#: src/pages/onboarding/InviteTeam.tsx
msgid "Copy invitation link"
@ -2412,6 +2423,11 @@ msgstr "نسخ الرابط"
msgid "Copy link to view"
msgstr "نسخ الرابط لعرضه"
#. js-lingui-id: VBIlvI
#: src/modules/ai/components/TerminalOutput.tsx
msgid "Copy output"
msgstr ""
#. js-lingui-id: HvB+os
#: src/pages/settings/SettingsTwoFactorAuthenticationMethod.tsx
msgid "Copy paste the code below"
@ -3452,6 +3468,11 @@ msgstr "النقاط والفاصلة - {dotsAndCommaExample}"
msgid "Downgrade"
msgstr "خفض المستوى"
#. js-lingui-id: qnTwse
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Download {filename}"
msgstr ""
#. js-lingui-id: WcWS//
#: src/modules/settings/security/components/SSO/SettingsSSOSAMLForm.tsx
msgid "Download file"
@ -4352,6 +4373,7 @@ msgstr "الإعتمادات الإضافية المستخدمة"
#. js-lingui-id: 7Bj3x9
#: src/pages/settings/emailing-domains/utils/getEmailingDomainStatusText.ts
#: src/modules/settings/admin-panel/health-status/components/SettingsAdminQueueJobsTable.tsx
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Failed"
msgstr "فشل"
@ -4714,6 +4736,11 @@ msgstr "عام"
msgid "General - Settings"
msgstr "الإعدادات العامة"
#. js-lingui-id: wu9eMO
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Generated Files"
msgstr ""
#. js-lingui-id: DDcvSo
#: src/pages/settings/profile/appearance/components/LocalePicker.tsx
msgid "German"
@ -6670,6 +6697,11 @@ msgstr "لا توجد نماذج متاحة. يرجى تهيئة نماذج ال
msgid "No option found"
msgstr "لم يتم العثور على خيار"
#. js-lingui-id: XRc1G9
#: src/modules/ai/components/TerminalOutput.tsx
msgid "No output"
msgstr ""
#. js-lingui-id: tTItk7
#: src/modules/workflow/workflow-steps/components/WorkflowRunStepOutputDetail.tsx
msgid "No output available"
@ -7140,6 +7172,12 @@ msgstr "مساحات العمل الأخرى"
msgid "Our team can help you set up your workspace to match your specific needs and workflows."
msgstr "يمكن لفريقنا مساعدتك في إعداد بيئة العمل الخاصة بك لتناسب احتياجاتك المحددة وسير العمل الخاص بك."
#. js-lingui-id: gh06VD
#: src/modules/ai/components/TerminalOutput.tsx
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Output"
msgstr ""
#. js-lingui-id: xvfn7l
#: src/modules/workflow/workflow-steps/workflow-actions/ai-agent-action/components/WorkflowOutputSchemaBuilder.tsx
msgid "Output Field {fieldNumber}"
@ -7585,6 +7623,11 @@ msgstr "تم حذف النطاق العام بنجاح"
msgid "Public Domains"
msgstr "النطاقات العامة"
#. js-lingui-id: GUpZQM
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Python Code Execution"
msgstr ""
#. js-lingui-id: 2vudsu
#: src/modules/command-menu/pages/page-layout/utils/getDateGranularityLabel.ts
msgid "Quarter"
@ -8029,6 +8072,11 @@ msgstr "\\\\"
msgid "Run Function"
msgstr "تشغيل الوظيفة"
#. js-lingui-id: LTC198
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Running..."
msgstr ""
#. js-lingui-id: nji0/X
#: src/pages/settings/profile/appearance/components/LocalePicker.tsx
msgid "Russian"
@ -10192,6 +10240,11 @@ msgstr "في انتظار"
msgid "Waiting Children"
msgstr "انتظار الأطفال"
#. js-lingui-id: TdQd9Q
#: src/modules/ai/components/TerminalOutput.tsx
msgid "Waiting for output..."
msgstr ""
#. js-lingui-id: zFSQY3
#: src/modules/activities/timeline-activities/rows/main-object/components/EventRowMainObject.tsx
msgid "was created by"

View file

@ -2070,6 +2070,11 @@ msgstr "Tanca"
msgid "Close command menu"
msgstr "Tanca el menú de comandament"
#. js-lingui-id: EWPtMO
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Code"
msgstr ""
#. js-lingui-id: /8PmQ9
#: src/modules/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowEditActionServerlessFunction.tsx
msgid "Code Editor"
@ -2147,6 +2152,7 @@ msgstr "completat"
#. js-lingui-id: qqWcBV
#: src/modules/settings/admin-panel/health-status/components/SettingsAdminQueueJobsTable.tsx
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Completed"
msgstr "Completat"
@ -2397,6 +2403,11 @@ msgstr "Copiat al porta-retalls"
msgid "Copy"
msgstr "Copia"
#. js-lingui-id: NmPNJJ
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Copy code"
msgstr ""
#. js-lingui-id: 7eVkEH
#: src/pages/onboarding/InviteTeam.tsx
msgid "Copy invitation link"
@ -2412,6 +2423,11 @@ msgstr "Copia l'enllaç"
msgid "Copy link to view"
msgstr "Copia l'enllaç per veure"
#. js-lingui-id: VBIlvI
#: src/modules/ai/components/TerminalOutput.tsx
msgid "Copy output"
msgstr ""
#. js-lingui-id: HvB+os
#: src/pages/settings/SettingsTwoFactorAuthenticationMethod.tsx
msgid "Copy paste the code below"
@ -3452,6 +3468,11 @@ msgstr "Punts i coma - {dotsAndCommaExample}"
msgid "Downgrade"
msgstr "Baixar de categoria"
#. js-lingui-id: qnTwse
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Download {filename}"
msgstr ""
#. js-lingui-id: WcWS//
#: src/modules/settings/security/components/SSO/SettingsSSOSAMLForm.tsx
msgid "Download file"
@ -4352,6 +4373,7 @@ msgstr "Crèdits extres utilitzats"
#. js-lingui-id: 7Bj3x9
#: src/pages/settings/emailing-domains/utils/getEmailingDomainStatusText.ts
#: src/modules/settings/admin-panel/health-status/components/SettingsAdminQueueJobsTable.tsx
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Failed"
msgstr "Error"
@ -4714,6 +4736,11 @@ msgstr "General"
msgid "General - Settings"
msgstr "General - Configuració"
#. js-lingui-id: wu9eMO
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Generated Files"
msgstr ""
#. js-lingui-id: DDcvSo
#: src/pages/settings/profile/appearance/components/LocalePicker.tsx
msgid "German"
@ -6670,6 +6697,11 @@ msgstr "No hi ha models disponibles. Si us plau, configureu models d'IA a la con
msgid "No option found"
msgstr "No s'ha trobat cap opció"
#. js-lingui-id: XRc1G9
#: src/modules/ai/components/TerminalOutput.tsx
msgid "No output"
msgstr ""
#. js-lingui-id: tTItk7
#: src/modules/workflow/workflow-steps/components/WorkflowRunStepOutputDetail.tsx
msgid "No output available"
@ -7140,6 +7172,12 @@ msgstr "Altres espais de treball"
msgid "Our team can help you set up your workspace to match your specific needs and workflows."
msgstr "El nostre equip pot ajudar-te a configurar el teu espai de treball perquè s'adapti a les teves necessitats i fluxos de treball específics."
#. js-lingui-id: gh06VD
#: src/modules/ai/components/TerminalOutput.tsx
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Output"
msgstr ""
#. js-lingui-id: xvfn7l
#: src/modules/workflow/workflow-steps/workflow-actions/ai-agent-action/components/WorkflowOutputSchemaBuilder.tsx
msgid "Output Field {fieldNumber}"
@ -7585,6 +7623,11 @@ msgstr "Domini públic eliminat correctament"
msgid "Public Domains"
msgstr "Dominis públics"
#. js-lingui-id: GUpZQM
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Python Code Execution"
msgstr ""
#. js-lingui-id: 2vudsu
#: src/modules/command-menu/pages/page-layout/utils/getDateGranularityLabel.ts
msgid "Quarter"
@ -8029,6 +8072,11 @@ msgstr "Executa un workflow i torna aquí per veure les seves execucions"
msgid "Run Function"
msgstr "Executa Funció"
#. js-lingui-id: LTC198
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Running..."
msgstr ""
#. js-lingui-id: nji0/X
#: src/pages/settings/profile/appearance/components/LocalePicker.tsx
msgid "Russian"
@ -10192,6 +10240,11 @@ msgstr "Esperant"
msgid "Waiting Children"
msgstr "Infants en espera"
#. js-lingui-id: TdQd9Q
#: src/modules/ai/components/TerminalOutput.tsx
msgid "Waiting for output..."
msgstr ""
#. js-lingui-id: zFSQY3
#: src/modules/activities/timeline-activities/rows/main-object/components/EventRowMainObject.tsx
msgid "was created by"

View file

@ -2070,6 +2070,11 @@ msgstr "Zavřít"
msgid "Close command menu"
msgstr "Zavřít příkazové menu"
#. js-lingui-id: EWPtMO
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Code"
msgstr ""
#. js-lingui-id: /8PmQ9
#: src/modules/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowEditActionServerlessFunction.tsx
msgid "Code Editor"
@ -2147,6 +2152,7 @@ msgstr "dokončeno"
#. js-lingui-id: qqWcBV
#: src/modules/settings/admin-panel/health-status/components/SettingsAdminQueueJobsTable.tsx
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Completed"
msgstr "Dokončeno"
@ -2397,6 +2403,11 @@ msgstr "Zkopírováno do schránky"
msgid "Copy"
msgstr "Kopírovat"
#. js-lingui-id: NmPNJJ
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Copy code"
msgstr ""
#. js-lingui-id: 7eVkEH
#: src/pages/onboarding/InviteTeam.tsx
msgid "Copy invitation link"
@ -2412,6 +2423,11 @@ msgstr "Kopírovat odkaz"
msgid "Copy link to view"
msgstr "Kopírovat odkaz pro zobrazení"
#. js-lingui-id: VBIlvI
#: src/modules/ai/components/TerminalOutput.tsx
msgid "Copy output"
msgstr ""
#. js-lingui-id: HvB+os
#: src/pages/settings/SettingsTwoFactorAuthenticationMethod.tsx
msgid "Copy paste the code below"
@ -3452,6 +3468,11 @@ msgstr "Tečky a čárky - {dotsAndCommaExample}"
msgid "Downgrade"
msgstr "Snížení úrovně"
#. js-lingui-id: qnTwse
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Download {filename}"
msgstr ""
#. js-lingui-id: WcWS//
#: src/modules/settings/security/components/SSO/SettingsSSOSAMLForm.tsx
msgid "Download file"
@ -4352,6 +4373,7 @@ msgstr "Použité extra kredity"
#. js-lingui-id: 7Bj3x9
#: src/pages/settings/emailing-domains/utils/getEmailingDomainStatusText.ts
#: src/modules/settings/admin-panel/health-status/components/SettingsAdminQueueJobsTable.tsx
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Failed"
msgstr "Selhalo"
@ -4714,6 +4736,11 @@ msgstr "Obecné"
msgid "General - Settings"
msgstr "Obecné - Nastavení"
#. js-lingui-id: wu9eMO
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Generated Files"
msgstr ""
#. js-lingui-id: DDcvSo
#: src/pages/settings/profile/appearance/components/LocalePicker.tsx
msgid "German"
@ -6670,6 +6697,11 @@ msgstr "Nejsou dostupné žádné modely. Prosím, nakonfigurujte AI modely v na
msgid "No option found"
msgstr "Žádná možnost nenalezena"
#. js-lingui-id: XRc1G9
#: src/modules/ai/components/TerminalOutput.tsx
msgid "No output"
msgstr ""
#. js-lingui-id: tTItk7
#: src/modules/workflow/workflow-steps/components/WorkflowRunStepOutputDetail.tsx
msgid "No output available"
@ -7140,6 +7172,12 @@ msgstr "Jiné pracovní prostory"
msgid "Our team can help you set up your workspace to match your specific needs and workflows."
msgstr "Náš tým vám může pomoci nastavit pracovní prostor tak, aby odpovídal vašim specifickým potřebám a pracovním postupům."
#. js-lingui-id: gh06VD
#: src/modules/ai/components/TerminalOutput.tsx
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Output"
msgstr ""
#. js-lingui-id: xvfn7l
#: src/modules/workflow/workflow-steps/workflow-actions/ai-agent-action/components/WorkflowOutputSchemaBuilder.tsx
msgid "Output Field {fieldNumber}"
@ -7585,6 +7623,11 @@ msgstr "Veřejná doména byla úspěšně smazána"
msgid "Public Domains"
msgstr "Veřejné domény"
#. js-lingui-id: GUpZQM
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Python Code Execution"
msgstr ""
#. js-lingui-id: 2vudsu
#: src/modules/command-menu/pages/page-layout/utils/getDateGranularityLabel.ts
msgid "Quarter"
@ -8029,6 +8072,11 @@ msgstr "Spusťte pracovní postup a vraťte se sem pro zobrazení jeho proveden
msgid "Run Function"
msgstr "Spustit funkci"
#. js-lingui-id: LTC198
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Running..."
msgstr ""
#. js-lingui-id: nji0/X
#: src/pages/settings/profile/appearance/components/LocalePicker.tsx
msgid "Russian"
@ -10192,6 +10240,11 @@ msgstr "Čekání"
msgid "Waiting Children"
msgstr "Čekající potomci"
#. js-lingui-id: TdQd9Q
#: src/modules/ai/components/TerminalOutput.tsx
msgid "Waiting for output..."
msgstr ""
#. js-lingui-id: zFSQY3
#: src/modules/activities/timeline-activities/rows/main-object/components/EventRowMainObject.tsx
msgid "was created by"

View file

@ -2070,6 +2070,11 @@ msgstr "Luk"
msgid "Close command menu"
msgstr "Luk kommandomenu"
#. js-lingui-id: EWPtMO
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Code"
msgstr ""
#. js-lingui-id: /8PmQ9
#: src/modules/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowEditActionServerlessFunction.tsx
msgid "Code Editor"
@ -2147,6 +2152,7 @@ msgstr "fuldført"
#. js-lingui-id: qqWcBV
#: src/modules/settings/admin-panel/health-status/components/SettingsAdminQueueJobsTable.tsx
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Completed"
msgstr "Fuldført"
@ -2397,6 +2403,11 @@ msgstr "Kopieret til udklipsholder"
msgid "Copy"
msgstr "Kopier"
#. js-lingui-id: NmPNJJ
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Copy code"
msgstr ""
#. js-lingui-id: 7eVkEH
#: src/pages/onboarding/InviteTeam.tsx
msgid "Copy invitation link"
@ -2412,6 +2423,11 @@ msgstr "Kopier link"
msgid "Copy link to view"
msgstr "Kopier link til visning"
#. js-lingui-id: VBIlvI
#: src/modules/ai/components/TerminalOutput.tsx
msgid "Copy output"
msgstr ""
#. js-lingui-id: HvB+os
#: src/pages/settings/SettingsTwoFactorAuthenticationMethod.tsx
msgid "Copy paste the code below"
@ -3452,6 +3468,11 @@ msgstr "Punktummer og komma - {dotsAndCommaExample}"
msgid "Downgrade"
msgstr "Nedgrader"
#. js-lingui-id: qnTwse
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Download {filename}"
msgstr ""
#. js-lingui-id: WcWS//
#: src/modules/settings/security/components/SSO/SettingsSSOSAMLForm.tsx
msgid "Download file"
@ -4352,6 +4373,7 @@ msgstr "Ekstra kreditter forbrugt"
#. js-lingui-id: 7Bj3x9
#: src/pages/settings/emailing-domains/utils/getEmailingDomainStatusText.ts
#: src/modules/settings/admin-panel/health-status/components/SettingsAdminQueueJobsTable.tsx
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Failed"
msgstr "Mislykkedes"
@ -4714,6 +4736,11 @@ msgstr "Generelt"
msgid "General - Settings"
msgstr "Generelt - Indstillinger"
#. js-lingui-id: wu9eMO
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Generated Files"
msgstr ""
#. js-lingui-id: DDcvSo
#: src/pages/settings/profile/appearance/components/LocalePicker.tsx
msgid "German"
@ -6670,6 +6697,11 @@ msgstr "Ingen modeller tilgængelige. Konfigurer venligst AI-modeller i dine arb
msgid "No option found"
msgstr "Ingen mulighed fundet"
#. js-lingui-id: XRc1G9
#: src/modules/ai/components/TerminalOutput.tsx
msgid "No output"
msgstr ""
#. js-lingui-id: tTItk7
#: src/modules/workflow/workflow-steps/components/WorkflowRunStepOutputDetail.tsx
msgid "No output available"
@ -7140,6 +7172,12 @@ msgstr "Andre arbejdsområder"
msgid "Our team can help you set up your workspace to match your specific needs and workflows."
msgstr "Vores team kan hjælpe dig med at sætte din arbejdsområde op, så den matcher dine specifikke behov og workflows."
#. js-lingui-id: gh06VD
#: src/modules/ai/components/TerminalOutput.tsx
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Output"
msgstr ""
#. js-lingui-id: xvfn7l
#: src/modules/workflow/workflow-steps/workflow-actions/ai-agent-action/components/WorkflowOutputSchemaBuilder.tsx
msgid "Output Field {fieldNumber}"
@ -7585,6 +7623,11 @@ msgstr "Offentligt domæne blev slettet med succes"
msgid "Public Domains"
msgstr "Offentlige Domæner"
#. js-lingui-id: GUpZQM
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Python Code Execution"
msgstr ""
#. js-lingui-id: 2vudsu
#: src/modules/command-menu/pages/page-layout/utils/getDateGranularityLabel.ts
msgid "Quarter"
@ -8029,6 +8072,11 @@ msgstr "Kør en arbejdsproces og vend tilbage hertil for at se dens eksekveringe
msgid "Run Function"
msgstr "Kør funktion"
#. js-lingui-id: LTC198
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Running..."
msgstr ""
#. js-lingui-id: nji0/X
#: src/pages/settings/profile/appearance/components/LocalePicker.tsx
msgid "Russian"
@ -10194,6 +10242,11 @@ msgstr "Venter"
msgid "Waiting Children"
msgstr "Venter på børn"
#. js-lingui-id: TdQd9Q
#: src/modules/ai/components/TerminalOutput.tsx
msgid "Waiting for output..."
msgstr ""
#. js-lingui-id: zFSQY3
#: src/modules/activities/timeline-activities/rows/main-object/components/EventRowMainObject.tsx
msgid "was created by"

View file

@ -2070,6 +2070,11 @@ msgstr "Schließen"
msgid "Close command menu"
msgstr "Befehlsmenü schließen"
#. js-lingui-id: EWPtMO
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Code"
msgstr ""
#. js-lingui-id: /8PmQ9
#: src/modules/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowEditActionServerlessFunction.tsx
msgid "Code Editor"
@ -2147,6 +2152,7 @@ msgstr "abgeschlossen"
#. js-lingui-id: qqWcBV
#: src/modules/settings/admin-panel/health-status/components/SettingsAdminQueueJobsTable.tsx
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Completed"
msgstr "Abgeschlossen"
@ -2397,6 +2403,11 @@ msgstr "In die Zwischenablage kopiert"
msgid "Copy"
msgstr "Kopieren"
#. js-lingui-id: NmPNJJ
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Copy code"
msgstr ""
#. js-lingui-id: 7eVkEH
#: src/pages/onboarding/InviteTeam.tsx
msgid "Copy invitation link"
@ -2412,6 +2423,11 @@ msgstr "Link kopieren"
msgid "Copy link to view"
msgstr "Link kopieren, um Ansicht zu öffnen"
#. js-lingui-id: VBIlvI
#: src/modules/ai/components/TerminalOutput.tsx
msgid "Copy output"
msgstr ""
#. js-lingui-id: HvB+os
#: src/pages/settings/SettingsTwoFactorAuthenticationMethod.tsx
msgid "Copy paste the code below"
@ -3452,6 +3468,11 @@ msgstr "Punkte und Komma - {dotsAndCommaExample}"
msgid "Downgrade"
msgstr "Herabstufen"
#. js-lingui-id: qnTwse
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Download {filename}"
msgstr ""
#. js-lingui-id: WcWS//
#: src/modules/settings/security/components/SSO/SettingsSSOSAMLForm.tsx
msgid "Download file"
@ -4352,6 +4373,7 @@ msgstr "Verwendete zusätzliche Credits"
#. js-lingui-id: 7Bj3x9
#: src/pages/settings/emailing-domains/utils/getEmailingDomainStatusText.ts
#: src/modules/settings/admin-panel/health-status/components/SettingsAdminQueueJobsTable.tsx
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Failed"
msgstr "Fehlgeschlagen"
@ -4714,6 +4736,11 @@ msgstr "Allgemein"
msgid "General - Settings"
msgstr "Allgemein - Einstellungen"
#. js-lingui-id: wu9eMO
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Generated Files"
msgstr ""
#. js-lingui-id: DDcvSo
#: src/pages/settings/profile/appearance/components/LocalePicker.tsx
msgid "German"
@ -6670,6 +6697,11 @@ msgstr "Keine Modelle verfügbar. Bitte konfigurieren Sie KI-Modelle in Ihren Ar
msgid "No option found"
msgstr "Keine Option gefunden"
#. js-lingui-id: XRc1G9
#: src/modules/ai/components/TerminalOutput.tsx
msgid "No output"
msgstr ""
#. js-lingui-id: tTItk7
#: src/modules/workflow/workflow-steps/components/WorkflowRunStepOutputDetail.tsx
msgid "No output available"
@ -7140,6 +7172,12 @@ msgstr "Andere Arbeitsbereiche"
msgid "Our team can help you set up your workspace to match your specific needs and workflows."
msgstr "Unser Team kann Ihnen dabei helfen, Ihren Arbeitsbereich entsprechend Ihren spezifischen Anforderungen und Arbeitsabläufen einzurichten."
#. js-lingui-id: gh06VD
#: src/modules/ai/components/TerminalOutput.tsx
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Output"
msgstr ""
#. js-lingui-id: xvfn7l
#: src/modules/workflow/workflow-steps/workflow-actions/ai-agent-action/components/WorkflowOutputSchemaBuilder.tsx
msgid "Output Field {fieldNumber}"
@ -7585,6 +7623,11 @@ msgstr "Öffentliche Domäne erfolgreich gelöscht"
msgid "Public Domains"
msgstr "Öffentliche Domains"
#. js-lingui-id: GUpZQM
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Python Code Execution"
msgstr ""
#. js-lingui-id: 2vudsu
#: src/modules/command-menu/pages/page-layout/utils/getDateGranularityLabel.ts
msgid "Quarter"
@ -8029,6 +8072,11 @@ msgstr "Führen Sie einen Workflow aus und kehren Sie zurück, um die Ausführun
msgid "Run Function"
msgstr "Funktion ausführen"
#. js-lingui-id: LTC198
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Running..."
msgstr ""
#. js-lingui-id: nji0/X
#: src/pages/settings/profile/appearance/components/LocalePicker.tsx
msgid "Russian"
@ -10192,6 +10240,11 @@ msgstr "Wartend"
msgid "Waiting Children"
msgstr "Wartende Kinder"
#. js-lingui-id: TdQd9Q
#: src/modules/ai/components/TerminalOutput.tsx
msgid "Waiting for output..."
msgstr ""
#. js-lingui-id: zFSQY3
#: src/modules/activities/timeline-activities/rows/main-object/components/EventRowMainObject.tsx
msgid "was created by"

View file

@ -2070,6 +2070,11 @@ msgstr "Κλείσιμο"
msgid "Close command menu"
msgstr "Κλείσιμο μενού εντολών"
#. js-lingui-id: EWPtMO
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Code"
msgstr ""
#. js-lingui-id: /8PmQ9
#: src/modules/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowEditActionServerlessFunction.tsx
msgid "Code Editor"
@ -2147,6 +2152,7 @@ msgstr "ολοκληρωμένο"
#. js-lingui-id: qqWcBV
#: src/modules/settings/admin-panel/health-status/components/SettingsAdminQueueJobsTable.tsx
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Completed"
msgstr "Αποπερατωμένες"
@ -2397,6 +2403,11 @@ msgstr "Αντιγράφηκε στο πρόχειρο"
msgid "Copy"
msgstr "Αντιγραφή"
#. js-lingui-id: NmPNJJ
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Copy code"
msgstr ""
#. js-lingui-id: 7eVkEH
#: src/pages/onboarding/InviteTeam.tsx
msgid "Copy invitation link"
@ -2412,6 +2423,11 @@ msgstr "Αντιγραφή συνδέσμου"
msgid "Copy link to view"
msgstr "Αντιγραφή σύνδεσμου για προβολή"
#. js-lingui-id: VBIlvI
#: src/modules/ai/components/TerminalOutput.tsx
msgid "Copy output"
msgstr ""
#. js-lingui-id: HvB+os
#: src/pages/settings/SettingsTwoFactorAuthenticationMethod.tsx
msgid "Copy paste the code below"
@ -3452,6 +3468,11 @@ msgstr "Τελείες και κόμμα - {dotsAndCommaExample}"
msgid "Downgrade"
msgstr "Υποβάθμιση"
#. js-lingui-id: qnTwse
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Download {filename}"
msgstr ""
#. js-lingui-id: WcWS//
#: src/modules/settings/security/components/SSO/SettingsSSOSAMLForm.tsx
msgid "Download file"
@ -4352,6 +4373,7 @@ msgstr "Πρόσθετες μονάδες χρησιμοποιήθηκαν"
#. js-lingui-id: 7Bj3x9
#: src/pages/settings/emailing-domains/utils/getEmailingDomainStatusText.ts
#: src/modules/settings/admin-panel/health-status/components/SettingsAdminQueueJobsTable.tsx
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Failed"
msgstr "Απέτυχε"
@ -4714,6 +4736,11 @@ msgstr "Γενικά"
msgid "General - Settings"
msgstr "Γενικά - Ρυθμίσεις"
#. js-lingui-id: wu9eMO
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Generated Files"
msgstr ""
#. js-lingui-id: DDcvSo
#: src/pages/settings/profile/appearance/components/LocalePicker.tsx
msgid "German"
@ -6670,6 +6697,11 @@ msgstr "Δεν υπάρχουν διαθέσιμα μοντέλα. Παρακα
msgid "No option found"
msgstr "Δε βρέθηκε επιλογή"
#. js-lingui-id: XRc1G9
#: src/modules/ai/components/TerminalOutput.tsx
msgid "No output"
msgstr ""
#. js-lingui-id: tTItk7
#: src/modules/workflow/workflow-steps/components/WorkflowRunStepOutputDetail.tsx
msgid "No output available"
@ -7140,6 +7172,12 @@ msgstr "Άλλοι χώροι εργασίας"
msgid "Our team can help you set up your workspace to match your specific needs and workflows."
msgstr "Η ομάδα μας μπορεί να σας βοηθήσει να ρυθμίσετε τον χώρο εργασίας σας ώστε να ταιριάζει στις συγκεκριμένες ανάγκες και τις ροές εργασιών σας."
#. js-lingui-id: gh06VD
#: src/modules/ai/components/TerminalOutput.tsx
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Output"
msgstr ""
#. js-lingui-id: xvfn7l
#: src/modules/workflow/workflow-steps/workflow-actions/ai-agent-action/components/WorkflowOutputSchemaBuilder.tsx
msgid "Output Field {fieldNumber}"
@ -7585,6 +7623,11 @@ msgstr "Τα δημόσια δικαιώματα διαγράφηκαν με ε
msgid "Public Domains"
msgstr "Δημόσια Δικαιώματα"
#. js-lingui-id: GUpZQM
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Python Code Execution"
msgstr ""
#. js-lingui-id: 2vudsu
#: src/modules/command-menu/pages/page-layout/utils/getDateGranularityLabel.ts
msgid "Quarter"
@ -8029,6 +8072,11 @@ msgstr "Εκτελέστε ένα workflow και επιστρέψτε εδώ γ
msgid "Run Function"
msgstr "Εκτελέστε τη Λειτουργία"
#. js-lingui-id: LTC198
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Running..."
msgstr ""
#. js-lingui-id: nji0/X
#: src/pages/settings/profile/appearance/components/LocalePicker.tsx
msgid "Russian"
@ -10196,6 +10244,11 @@ msgstr "Σε αναμονή"
msgid "Waiting Children"
msgstr "Αναμένονται Παιδιά"
#. js-lingui-id: TdQd9Q
#: src/modules/ai/components/TerminalOutput.tsx
msgid "Waiting for output..."
msgstr ""
#. js-lingui-id: zFSQY3
#: src/modules/activities/timeline-activities/rows/main-object/components/EventRowMainObject.tsx
msgid "was created by"

View file

@ -2065,6 +2065,11 @@ msgstr "Close"
msgid "Close command menu"
msgstr "Close command menu"
#. js-lingui-id: EWPtMO
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Code"
msgstr "Code"
#. js-lingui-id: /8PmQ9
#: src/modules/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowEditActionServerlessFunction.tsx
msgid "Code Editor"
@ -2142,6 +2147,7 @@ msgstr "completed"
#. js-lingui-id: qqWcBV
#: src/modules/settings/admin-panel/health-status/components/SettingsAdminQueueJobsTable.tsx
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Completed"
msgstr "Completed"
@ -2392,6 +2398,11 @@ msgstr "Copied to clipboard"
msgid "Copy"
msgstr "Copy"
#. js-lingui-id: NmPNJJ
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Copy code"
msgstr "Copy code"
#. js-lingui-id: 7eVkEH
#: src/pages/onboarding/InviteTeam.tsx
msgid "Copy invitation link"
@ -2407,6 +2418,11 @@ msgstr "Copy link"
msgid "Copy link to view"
msgstr "Copy link to view"
#. js-lingui-id: VBIlvI
#: src/modules/ai/components/TerminalOutput.tsx
msgid "Copy output"
msgstr "Copy output"
#. js-lingui-id: HvB+os
#: src/pages/settings/SettingsTwoFactorAuthenticationMethod.tsx
msgid "Copy paste the code below"
@ -3447,6 +3463,11 @@ msgstr "Dots and comma - {dotsAndCommaExample}"
msgid "Downgrade"
msgstr "Downgrade"
#. js-lingui-id: qnTwse
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Download {filename}"
msgstr "Download {filename}"
#. js-lingui-id: WcWS//
#: src/modules/settings/security/components/SSO/SettingsSSOSAMLForm.tsx
msgid "Download file"
@ -4347,6 +4368,7 @@ msgstr "Extra Credits Used"
#. js-lingui-id: 7Bj3x9
#: src/pages/settings/emailing-domains/utils/getEmailingDomainStatusText.ts
#: src/modules/settings/admin-panel/health-status/components/SettingsAdminQueueJobsTable.tsx
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Failed"
msgstr "Failed"
@ -4709,6 +4731,11 @@ msgstr "General"
msgid "General - Settings"
msgstr "General - Settings"
#. js-lingui-id: wu9eMO
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Generated Files"
msgstr "Generated Files"
#. js-lingui-id: DDcvSo
#: src/pages/settings/profile/appearance/components/LocalePicker.tsx
msgid "German"
@ -6665,6 +6692,11 @@ msgstr "No models available. Please configure AI models in your workspace settin
msgid "No option found"
msgstr "No option found"
#. js-lingui-id: XRc1G9
#: src/modules/ai/components/TerminalOutput.tsx
msgid "No output"
msgstr "No output"
#. js-lingui-id: tTItk7
#: src/modules/workflow/workflow-steps/components/WorkflowRunStepOutputDetail.tsx
msgid "No output available"
@ -7135,6 +7167,12 @@ msgstr "Other workspaces"
msgid "Our team can help you set up your workspace to match your specific needs and workflows."
msgstr "Our team can help you set up your workspace to match your specific needs and workflows."
#. js-lingui-id: gh06VD
#: src/modules/ai/components/TerminalOutput.tsx
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Output"
msgstr "Output"
#. js-lingui-id: xvfn7l
#: src/modules/workflow/workflow-steps/workflow-actions/ai-agent-action/components/WorkflowOutputSchemaBuilder.tsx
msgid "Output Field {fieldNumber}"
@ -7580,6 +7618,11 @@ msgstr "Public domain successfully deleted"
msgid "Public Domains"
msgstr "Public Domains"
#. js-lingui-id: GUpZQM
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Python Code Execution"
msgstr "Python Code Execution"
#. js-lingui-id: 2vudsu
#: src/modules/command-menu/pages/page-layout/utils/getDateGranularityLabel.ts
msgid "Quarter"
@ -8024,6 +8067,11 @@ msgstr "Run a workflow and return here to view its executions"
msgid "Run Function"
msgstr "Run Function"
#. js-lingui-id: LTC198
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Running..."
msgstr "Running..."
#. js-lingui-id: nji0/X
#: src/pages/settings/profile/appearance/components/LocalePicker.tsx
msgid "Russian"
@ -10189,6 +10237,11 @@ msgstr "Waiting"
msgid "Waiting Children"
msgstr "Waiting Children"
#. js-lingui-id: TdQd9Q
#: src/modules/ai/components/TerminalOutput.tsx
msgid "Waiting for output..."
msgstr "Waiting for output..."
#. js-lingui-id: zFSQY3
#: src/modules/activities/timeline-activities/rows/main-object/components/EventRowMainObject.tsx
msgid "was created by"

View file

@ -2070,6 +2070,11 @@ msgstr "Cerrar"
msgid "Close command menu"
msgstr "Cerrar menú de comandos"
#. js-lingui-id: EWPtMO
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Code"
msgstr ""
#. js-lingui-id: /8PmQ9
#: src/modules/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowEditActionServerlessFunction.tsx
msgid "Code Editor"
@ -2147,6 +2152,7 @@ msgstr "completado"
#. js-lingui-id: qqWcBV
#: src/modules/settings/admin-panel/health-status/components/SettingsAdminQueueJobsTable.tsx
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Completed"
msgstr "Completado"
@ -2397,6 +2403,11 @@ msgstr "Copiado al portapapeles"
msgid "Copy"
msgstr "Copiar"
#. js-lingui-id: NmPNJJ
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Copy code"
msgstr ""
#. js-lingui-id: 7eVkEH
#: src/pages/onboarding/InviteTeam.tsx
msgid "Copy invitation link"
@ -2412,6 +2423,11 @@ msgstr "Copiar enlace"
msgid "Copy link to view"
msgstr "Copiar enlace para ver"
#. js-lingui-id: VBIlvI
#: src/modules/ai/components/TerminalOutput.tsx
msgid "Copy output"
msgstr ""
#. js-lingui-id: HvB+os
#: src/pages/settings/SettingsTwoFactorAuthenticationMethod.tsx
msgid "Copy paste the code below"
@ -3452,6 +3468,11 @@ msgstr "Puntos y coma - {dotsAndCommaExample}"
msgid "Downgrade"
msgstr "Degradar"
#. js-lingui-id: qnTwse
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Download {filename}"
msgstr ""
#. js-lingui-id: WcWS//
#: src/modules/settings/security/components/SSO/SettingsSSOSAMLForm.tsx
msgid "Download file"
@ -4352,6 +4373,7 @@ msgstr "Créditos Extra Usados"
#. js-lingui-id: 7Bj3x9
#: src/pages/settings/emailing-domains/utils/getEmailingDomainStatusText.ts
#: src/modules/settings/admin-panel/health-status/components/SettingsAdminQueueJobsTable.tsx
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Failed"
msgstr "Fallido"
@ -4714,6 +4736,11 @@ msgstr "General"
msgid "General - Settings"
msgstr "General - Ajustes"
#. js-lingui-id: wu9eMO
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Generated Files"
msgstr ""
#. js-lingui-id: DDcvSo
#: src/pages/settings/profile/appearance/components/LocalePicker.tsx
msgid "German"
@ -6670,6 +6697,11 @@ msgstr "No hay modelos disponibles. Por favor configure modelos de IA en la conf
msgid "No option found"
msgstr "No se encontró opción"
#. js-lingui-id: XRc1G9
#: src/modules/ai/components/TerminalOutput.tsx
msgid "No output"
msgstr ""
#. js-lingui-id: tTItk7
#: src/modules/workflow/workflow-steps/components/WorkflowRunStepOutputDetail.tsx
msgid "No output available"
@ -7140,6 +7172,12 @@ msgstr "Otros espacios de trabajo"
msgid "Our team can help you set up your workspace to match your specific needs and workflows."
msgstr "Nuestro equipo puede ayudarle a configurar su espacio de trabajo para que se adapte a sus necesidades y flujos de trabajo específicos."
#. js-lingui-id: gh06VD
#: src/modules/ai/components/TerminalOutput.tsx
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Output"
msgstr ""
#. js-lingui-id: xvfn7l
#: src/modules/workflow/workflow-steps/workflow-actions/ai-agent-action/components/WorkflowOutputSchemaBuilder.tsx
msgid "Output Field {fieldNumber}"
@ -7585,6 +7623,11 @@ msgstr "Dominio público eliminado con éxito"
msgid "Public Domains"
msgstr "Dominios Públicos"
#. js-lingui-id: GUpZQM
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Python Code Execution"
msgstr ""
#. js-lingui-id: 2vudsu
#: src/modules/command-menu/pages/page-layout/utils/getDateGranularityLabel.ts
msgid "Quarter"
@ -8029,6 +8072,11 @@ msgstr "Ejecuta un workflow y vuelve aquí para ver sus ejecuciones"
msgid "Run Function"
msgstr "Ejecutar Función"
#. js-lingui-id: LTC198
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Running..."
msgstr ""
#. js-lingui-id: nji0/X
#: src/pages/settings/profile/appearance/components/LocalePicker.tsx
msgid "Russian"
@ -10194,6 +10242,11 @@ msgstr "Esperando"
msgid "Waiting Children"
msgstr "Esperando a hijos"
#. js-lingui-id: TdQd9Q
#: src/modules/ai/components/TerminalOutput.tsx
msgid "Waiting for output..."
msgstr ""
#. js-lingui-id: zFSQY3
#: src/modules/activities/timeline-activities/rows/main-object/components/EventRowMainObject.tsx
msgid "was created by"

View file

@ -2070,6 +2070,11 @@ msgstr "Sulje"
msgid "Close command menu"
msgstr "Sulje komento-valikko"
#. js-lingui-id: EWPtMO
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Code"
msgstr ""
#. js-lingui-id: /8PmQ9
#: src/modules/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowEditActionServerlessFunction.tsx
msgid "Code Editor"
@ -2147,6 +2152,7 @@ msgstr "valmis"
#. js-lingui-id: qqWcBV
#: src/modules/settings/admin-panel/health-status/components/SettingsAdminQueueJobsTable.tsx
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Completed"
msgstr "Valmis"
@ -2397,6 +2403,11 @@ msgstr "Kopioitu leikepöydälle"
msgid "Copy"
msgstr "Kopioi"
#. js-lingui-id: NmPNJJ
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Copy code"
msgstr ""
#. js-lingui-id: 7eVkEH
#: src/pages/onboarding/InviteTeam.tsx
msgid "Copy invitation link"
@ -2412,6 +2423,11 @@ msgstr "Kopioi linkki"
msgid "Copy link to view"
msgstr "Kopioi linkki katseluun"
#. js-lingui-id: VBIlvI
#: src/modules/ai/components/TerminalOutput.tsx
msgid "Copy output"
msgstr ""
#. js-lingui-id: HvB+os
#: src/pages/settings/SettingsTwoFactorAuthenticationMethod.tsx
msgid "Copy paste the code below"
@ -3452,6 +3468,11 @@ msgstr "Pisteitä ja pilkku - {dotsAndCommaExample}"
msgid "Downgrade"
msgstr "Alentaminen"
#. js-lingui-id: qnTwse
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Download {filename}"
msgstr ""
#. js-lingui-id: WcWS//
#: src/modules/settings/security/components/SSO/SettingsSSOSAMLForm.tsx
msgid "Download file"
@ -4352,6 +4373,7 @@ msgstr "Käytetyt ylimääräiset krediitit"
#. js-lingui-id: 7Bj3x9
#: src/pages/settings/emailing-domains/utils/getEmailingDomainStatusText.ts
#: src/modules/settings/admin-panel/health-status/components/SettingsAdminQueueJobsTable.tsx
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Failed"
msgstr "Epäonnistui"
@ -4714,6 +4736,11 @@ msgstr "Yleiset"
msgid "General - Settings"
msgstr "Yleiset - Asetukset"
#. js-lingui-id: wu9eMO
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Generated Files"
msgstr ""
#. js-lingui-id: DDcvSo
#: src/pages/settings/profile/appearance/components/LocalePicker.tsx
msgid "German"
@ -6670,6 +6697,11 @@ msgstr "Malitavoa ei ole saatavilla. Konfiguroi AI-mallit työtilan asetuksista.
msgid "No option found"
msgstr "Ei vaihtoehtoa löytynyt"
#. js-lingui-id: XRc1G9
#: src/modules/ai/components/TerminalOutput.tsx
msgid "No output"
msgstr ""
#. js-lingui-id: tTItk7
#: src/modules/workflow/workflow-steps/components/WorkflowRunStepOutputDetail.tsx
msgid "No output available"
@ -7140,6 +7172,12 @@ msgstr "Muut työtilat"
msgid "Our team can help you set up your workspace to match your specific needs and workflows."
msgstr "Tiimimme voi auttaa sinua järjestämään työtilasi vastaamaan erityistarpeitasi ja työnkulkuasi."
#. js-lingui-id: gh06VD
#: src/modules/ai/components/TerminalOutput.tsx
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Output"
msgstr ""
#. js-lingui-id: xvfn7l
#: src/modules/workflow/workflow-steps/workflow-actions/ai-agent-action/components/WorkflowOutputSchemaBuilder.tsx
msgid "Output Field {fieldNumber}"
@ -7585,6 +7623,11 @@ msgstr "Julkinen verkkotunnus poistettiin onnistuneesti"
msgid "Public Domains"
msgstr "Julkiset verkkotunnukset"
#. js-lingui-id: GUpZQM
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Python Code Execution"
msgstr ""
#. js-lingui-id: 2vudsu
#: src/modules/command-menu/pages/page-layout/utils/getDateGranularityLabel.ts
msgid "Quarter"
@ -8029,6 +8072,11 @@ msgstr "Suorita työnkulku ja palaa tänne nähdäksesi sen suoritukset"
msgid "Run Function"
msgstr "Suorita toiminto"
#. js-lingui-id: LTC198
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Running..."
msgstr ""
#. js-lingui-id: nji0/X
#: src/pages/settings/profile/appearance/components/LocalePicker.tsx
msgid "Russian"
@ -10192,6 +10240,11 @@ msgstr "Odottaa"
msgid "Waiting Children"
msgstr "Odotettavat lapset"
#. js-lingui-id: TdQd9Q
#: src/modules/ai/components/TerminalOutput.tsx
msgid "Waiting for output..."
msgstr ""
#. js-lingui-id: zFSQY3
#: src/modules/activities/timeline-activities/rows/main-object/components/EventRowMainObject.tsx
msgid "was created by"

View file

@ -2070,6 +2070,11 @@ msgstr "Fermer"
msgid "Close command menu"
msgstr "Fermer le menu de commande"
#. js-lingui-id: EWPtMO
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Code"
msgstr ""
#. js-lingui-id: /8PmQ9
#: src/modules/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowEditActionServerlessFunction.tsx
msgid "Code Editor"
@ -2147,6 +2152,7 @@ msgstr "terminé"
#. js-lingui-id: qqWcBV
#: src/modules/settings/admin-panel/health-status/components/SettingsAdminQueueJobsTable.tsx
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Completed"
msgstr "Terminé"
@ -2397,6 +2403,11 @@ msgstr "Copié dans le presse-papiers"
msgid "Copy"
msgstr "Copier"
#. js-lingui-id: NmPNJJ
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Copy code"
msgstr ""
#. js-lingui-id: 7eVkEH
#: src/pages/onboarding/InviteTeam.tsx
msgid "Copy invitation link"
@ -2412,6 +2423,11 @@ msgstr "Copier le lien"
msgid "Copy link to view"
msgstr "Copier le lien pour voir"
#. js-lingui-id: VBIlvI
#: src/modules/ai/components/TerminalOutput.tsx
msgid "Copy output"
msgstr ""
#. js-lingui-id: HvB+os
#: src/pages/settings/SettingsTwoFactorAuthenticationMethod.tsx
msgid "Copy paste the code below"
@ -3452,6 +3468,11 @@ msgstr "Points et virgule - {dotsAndCommaExample}"
msgid "Downgrade"
msgstr "Rétrograder"
#. js-lingui-id: qnTwse
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Download {filename}"
msgstr ""
#. js-lingui-id: WcWS//
#: src/modules/settings/security/components/SSO/SettingsSSOSAMLForm.tsx
msgid "Download file"
@ -4352,6 +4373,7 @@ msgstr "Crédits supplémentaires utilisés"
#. js-lingui-id: 7Bj3x9
#: src/pages/settings/emailing-domains/utils/getEmailingDomainStatusText.ts
#: src/modules/settings/admin-panel/health-status/components/SettingsAdminQueueJobsTable.tsx
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Failed"
msgstr "Échec"
@ -4714,6 +4736,11 @@ msgstr "Général"
msgid "General - Settings"
msgstr "Général - Paramètres"
#. js-lingui-id: wu9eMO
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Generated Files"
msgstr ""
#. js-lingui-id: DDcvSo
#: src/pages/settings/profile/appearance/components/LocalePicker.tsx
msgid "German"
@ -6670,6 +6697,11 @@ msgstr "Aucun modèle disponible. Veuillez configurer les modèles IA dans les p
msgid "No option found"
msgstr "Aucune option trouvée"
#. js-lingui-id: XRc1G9
#: src/modules/ai/components/TerminalOutput.tsx
msgid "No output"
msgstr ""
#. js-lingui-id: tTItk7
#: src/modules/workflow/workflow-steps/components/WorkflowRunStepOutputDetail.tsx
msgid "No output available"
@ -7140,6 +7172,12 @@ msgstr "Autres espaces de travail"
msgid "Our team can help you set up your workspace to match your specific needs and workflows."
msgstr "Notre équipe peut vous aider à configurer votre espace de travail pour qu'il corresponde à vos besoins spécifiques et à vos flux de travail."
#. js-lingui-id: gh06VD
#: src/modules/ai/components/TerminalOutput.tsx
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Output"
msgstr ""
#. js-lingui-id: xvfn7l
#: src/modules/workflow/workflow-steps/workflow-actions/ai-agent-action/components/WorkflowOutputSchemaBuilder.tsx
msgid "Output Field {fieldNumber}"
@ -7585,6 +7623,11 @@ msgstr "Domaine public supprimé avec succès"
msgid "Public Domains"
msgstr "Domaines publics"
#. js-lingui-id: GUpZQM
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Python Code Execution"
msgstr ""
#. js-lingui-id: 2vudsu
#: src/modules/command-menu/pages/page-layout/utils/getDateGranularityLabel.ts
msgid "Quarter"
@ -8029,6 +8072,11 @@ msgstr "Exécutez un workflow et revenez ici pour voir ses exécutions"
msgid "Run Function"
msgstr "Exécuter la fonction"
#. js-lingui-id: LTC198
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Running..."
msgstr ""
#. js-lingui-id: nji0/X
#: src/pages/settings/profile/appearance/components/LocalePicker.tsx
msgid "Russian"
@ -10194,6 +10242,11 @@ msgstr "En attente"
msgid "Waiting Children"
msgstr "Enfants en attente"
#. js-lingui-id: TdQd9Q
#: src/modules/ai/components/TerminalOutput.tsx
msgid "Waiting for output..."
msgstr ""
#. js-lingui-id: zFSQY3
#: src/modules/activities/timeline-activities/rows/main-object/components/EventRowMainObject.tsx
msgid "was created by"

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -2070,6 +2070,11 @@ msgstr "סגור"
msgid "Close command menu"
msgstr "סגור תפריט פקודות"
#. js-lingui-id: EWPtMO
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Code"
msgstr ""
#. js-lingui-id: /8PmQ9
#: src/modules/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowEditActionServerlessFunction.tsx
msgid "Code Editor"
@ -2147,6 +2152,7 @@ msgstr "הושלם"
#. js-lingui-id: qqWcBV
#: src/modules/settings/admin-panel/health-status/components/SettingsAdminQueueJobsTable.tsx
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Completed"
msgstr "הושלם"
@ -2397,6 +2403,11 @@ msgstr "הועתק ללוח הגזירים"
msgid "Copy"
msgstr "העתק"
#. js-lingui-id: NmPNJJ
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Copy code"
msgstr ""
#. js-lingui-id: 7eVkEH
#: src/pages/onboarding/InviteTeam.tsx
msgid "Copy invitation link"
@ -2412,6 +2423,11 @@ msgstr "העתק קישור"
msgid "Copy link to view"
msgstr "העתק קישור לצפייה"
#. js-lingui-id: VBIlvI
#: src/modules/ai/components/TerminalOutput.tsx
msgid "Copy output"
msgstr ""
#. js-lingui-id: HvB+os
#: src/pages/settings/SettingsTwoFactorAuthenticationMethod.tsx
msgid "Copy paste the code below"
@ -3452,6 +3468,11 @@ msgstr "נקודות ופסיק - {dotsAndCommaExample}"
msgid "Downgrade"
msgstr "שדרוג לאחור"
#. js-lingui-id: qnTwse
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Download {filename}"
msgstr ""
#. js-lingui-id: WcWS//
#: src/modules/settings/security/components/SSO/SettingsSSOSAMLForm.tsx
msgid "Download file"
@ -4352,6 +4373,7 @@ msgstr "קרדיטים נוספים שנוצלו"
#. js-lingui-id: 7Bj3x9
#: src/pages/settings/emailing-domains/utils/getEmailingDomainStatusText.ts
#: src/modules/settings/admin-panel/health-status/components/SettingsAdminQueueJobsTable.tsx
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Failed"
msgstr "<span dir=\"rtl\">נכשל</span>"
@ -4714,6 +4736,11 @@ msgstr "כללי"
msgid "General - Settings"
msgstr "כללי - הגדרות"
#. js-lingui-id: wu9eMO
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Generated Files"
msgstr ""
#. js-lingui-id: DDcvSo
#: src/pages/settings/profile/appearance/components/LocalePicker.tsx
msgid "German"
@ -6670,6 +6697,11 @@ msgstr "אין מודלים זמינים. נא להגדיר מודלים לבי
msgid "No option found"
msgstr "לא נמצאה אפשרות"
#. js-lingui-id: XRc1G9
#: src/modules/ai/components/TerminalOutput.tsx
msgid "No output"
msgstr ""
#. js-lingui-id: tTItk7
#: src/modules/workflow/workflow-steps/components/WorkflowRunStepOutputDetail.tsx
msgid "No output available"
@ -7140,6 +7172,12 @@ msgstr "מרחבים אחרים"
msgid "Our team can help you set up your workspace to match your specific needs and workflows."
msgstr "הצוות שלנו יכול לסייע לך להגדיר את סביבת העבודה שלך כך שתתאים לצרכים ולתהליכי העבודה שלך."
#. js-lingui-id: gh06VD
#: src/modules/ai/components/TerminalOutput.tsx
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Output"
msgstr ""
#. js-lingui-id: xvfn7l
#: src/modules/workflow/workflow-steps/workflow-actions/ai-agent-action/components/WorkflowOutputSchemaBuilder.tsx
msgid "Output Field {fieldNumber}"
@ -7585,6 +7623,11 @@ msgstr "דומיין ציבורי נמחק בהצלחה"
msgid "Public Domains"
msgstr "דומיינים ציבוריים"
#. js-lingui-id: GUpZQM
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Python Code Execution"
msgstr ""
#. js-lingui-id: 2vudsu
#: src/modules/command-menu/pages/page-layout/utils/getDateGranularityLabel.ts
msgid "Quarter"
@ -8029,6 +8072,11 @@ msgstr "\\"
msgid "Run Function"
msgstr "הפעל פונקציה"
#. js-lingui-id: LTC198
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Running..."
msgstr ""
#. js-lingui-id: nji0/X
#: src/pages/settings/profile/appearance/components/LocalePicker.tsx
msgid "Russian"
@ -10192,6 +10240,11 @@ msgstr "ממתין"
msgid "Waiting Children"
msgstr "ילדים ממתינים"
#. js-lingui-id: TdQd9Q
#: src/modules/ai/components/TerminalOutput.tsx
msgid "Waiting for output..."
msgstr ""
#. js-lingui-id: zFSQY3
#: src/modules/activities/timeline-activities/rows/main-object/components/EventRowMainObject.tsx
msgid "was created by"

View file

@ -2070,6 +2070,11 @@ msgstr "Bezár"
msgid "Close command menu"
msgstr "Parancsmenü bezárása"
#. js-lingui-id: EWPtMO
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Code"
msgstr ""
#. js-lingui-id: /8PmQ9
#: src/modules/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowEditActionServerlessFunction.tsx
msgid "Code Editor"
@ -2147,6 +2152,7 @@ msgstr "kész"
#. js-lingui-id: qqWcBV
#: src/modules/settings/admin-panel/health-status/components/SettingsAdminQueueJobsTable.tsx
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Completed"
msgstr "Kész"
@ -2397,6 +2403,11 @@ msgstr "Vágólapra másolva"
msgid "Copy"
msgstr "Másolás"
#. js-lingui-id: NmPNJJ
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Copy code"
msgstr ""
#. js-lingui-id: 7eVkEH
#: src/pages/onboarding/InviteTeam.tsx
msgid "Copy invitation link"
@ -2412,6 +2423,11 @@ msgstr "Hivatkozás másolása"
msgid "Copy link to view"
msgstr "Link másolása a megtekintéshez"
#. js-lingui-id: VBIlvI
#: src/modules/ai/components/TerminalOutput.tsx
msgid "Copy output"
msgstr ""
#. js-lingui-id: HvB+os
#: src/pages/settings/SettingsTwoFactorAuthenticationMethod.tsx
msgid "Copy paste the code below"
@ -3452,6 +3468,11 @@ msgstr "Pontok és vessző - {dotsAndCommaExample}"
msgid "Downgrade"
msgstr "Csomagcsökkentés"
#. js-lingui-id: qnTwse
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Download {filename}"
msgstr ""
#. js-lingui-id: WcWS//
#: src/modules/settings/security/components/SSO/SettingsSSOSAMLForm.tsx
msgid "Download file"
@ -4352,6 +4373,7 @@ msgstr "Felhasznált extra kreditek"
#. js-lingui-id: 7Bj3x9
#: src/pages/settings/emailing-domains/utils/getEmailingDomainStatusText.ts
#: src/modules/settings/admin-panel/health-status/components/SettingsAdminQueueJobsTable.tsx
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Failed"
msgstr "Sikertelen"
@ -4714,6 +4736,11 @@ msgstr "Általános"
msgid "General - Settings"
msgstr "Általános - Beállítások"
#. js-lingui-id: wu9eMO
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Generated Files"
msgstr ""
#. js-lingui-id: DDcvSo
#: src/pages/settings/profile/appearance/components/LocalePicker.tsx
msgid "German"
@ -6670,6 +6697,11 @@ msgstr "Nincsenek elérhető modellek. Kérjük, állítsa be a MI modelleket a
msgid "No option found"
msgstr "Nem található lehetőség"
#. js-lingui-id: XRc1G9
#: src/modules/ai/components/TerminalOutput.tsx
msgid "No output"
msgstr ""
#. js-lingui-id: tTItk7
#: src/modules/workflow/workflow-steps/components/WorkflowRunStepOutputDetail.tsx
msgid "No output available"
@ -7140,6 +7172,12 @@ msgstr "Egyéb munkaterületek"
msgid "Our team can help you set up your workspace to match your specific needs and workflows."
msgstr "Csapatunk segíthet az Ön munkaterületének beállításában, hogy az megfeleljen az Ön speciális igényeinek és munkafolyamatainak."
#. js-lingui-id: gh06VD
#: src/modules/ai/components/TerminalOutput.tsx
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Output"
msgstr ""
#. js-lingui-id: xvfn7l
#: src/modules/workflow/workflow-steps/workflow-actions/ai-agent-action/components/WorkflowOutputSchemaBuilder.tsx
msgid "Output Field {fieldNumber}"
@ -7585,6 +7623,11 @@ msgstr "A nyilvános domain sikeresen törölve"
msgid "Public Domains"
msgstr "Nyilvános Domainek"
#. js-lingui-id: GUpZQM
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Python Code Execution"
msgstr ""
#. js-lingui-id: 2vudsu
#: src/modules/command-menu/pages/page-layout/utils/getDateGranularityLabel.ts
msgid "Quarter"
@ -8029,6 +8072,11 @@ msgstr "Futtasson egy munkafolyamatot, és térjen vissza ide a végrehajtások
msgid "Run Function"
msgstr "Funkció futtatása"
#. js-lingui-id: LTC198
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Running..."
msgstr ""
#. js-lingui-id: nji0/X
#: src/pages/settings/profile/appearance/components/LocalePicker.tsx
msgid "Russian"
@ -10192,6 +10240,11 @@ msgstr "Várakozás"
msgid "Waiting Children"
msgstr "Várakozó gyermekek"
#. js-lingui-id: TdQd9Q
#: src/modules/ai/components/TerminalOutput.tsx
msgid "Waiting for output..."
msgstr ""
#. js-lingui-id: zFSQY3
#: src/modules/activities/timeline-activities/rows/main-object/components/EventRowMainObject.tsx
msgid "was created by"

View file

@ -2070,6 +2070,11 @@ msgstr "Chiudi"
msgid "Close command menu"
msgstr "Chiudi il menu comandi"
#. js-lingui-id: EWPtMO
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Code"
msgstr ""
#. js-lingui-id: /8PmQ9
#: src/modules/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowEditActionServerlessFunction.tsx
msgid "Code Editor"
@ -2147,6 +2152,7 @@ msgstr "completato"
#. js-lingui-id: qqWcBV
#: src/modules/settings/admin-panel/health-status/components/SettingsAdminQueueJobsTable.tsx
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Completed"
msgstr "Completato"
@ -2397,6 +2403,11 @@ msgstr "Copiato negli appunti"
msgid "Copy"
msgstr "Copia"
#. js-lingui-id: NmPNJJ
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Copy code"
msgstr ""
#. js-lingui-id: 7eVkEH
#: src/pages/onboarding/InviteTeam.tsx
msgid "Copy invitation link"
@ -2412,6 +2423,11 @@ msgstr "Copia link"
msgid "Copy link to view"
msgstr "Copia link per visualizzare"
#. js-lingui-id: VBIlvI
#: src/modules/ai/components/TerminalOutput.tsx
msgid "Copy output"
msgstr ""
#. js-lingui-id: HvB+os
#: src/pages/settings/SettingsTwoFactorAuthenticationMethod.tsx
msgid "Copy paste the code below"
@ -3452,6 +3468,11 @@ msgstr "Punti e virgola - {dotsAndCommaExample}"
msgid "Downgrade"
msgstr "Downgrade"
#. js-lingui-id: qnTwse
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Download {filename}"
msgstr ""
#. js-lingui-id: WcWS//
#: src/modules/settings/security/components/SSO/SettingsSSOSAMLForm.tsx
msgid "Download file"
@ -4352,6 +4373,7 @@ msgstr "Crediti extra usati"
#. js-lingui-id: 7Bj3x9
#: src/pages/settings/emailing-domains/utils/getEmailingDomainStatusText.ts
#: src/modules/settings/admin-panel/health-status/components/SettingsAdminQueueJobsTable.tsx
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Failed"
msgstr "Fallito"
@ -4714,6 +4736,11 @@ msgstr "Generale"
msgid "General - Settings"
msgstr "Generale - Impostazioni"
#. js-lingui-id: wu9eMO
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Generated Files"
msgstr ""
#. js-lingui-id: DDcvSo
#: src/pages/settings/profile/appearance/components/LocalePicker.tsx
msgid "German"
@ -6670,6 +6697,11 @@ msgstr "Nessun modello disponibile. Configurare i modelli AI nelle impostazioni
msgid "No option found"
msgstr "Nessuna opzione trovata"
#. js-lingui-id: XRc1G9
#: src/modules/ai/components/TerminalOutput.tsx
msgid "No output"
msgstr ""
#. js-lingui-id: tTItk7
#: src/modules/workflow/workflow-steps/components/WorkflowRunStepOutputDetail.tsx
msgid "No output available"
@ -7140,6 +7172,12 @@ msgstr "Altri spazi di lavoro"
msgid "Our team can help you set up your workspace to match your specific needs and workflows."
msgstr "Il nostro team può aiutarti a configurare il tuo spazio di lavoro per adattarsi alle tue esigenze e flussi di lavoro specifici."
#. js-lingui-id: gh06VD
#: src/modules/ai/components/TerminalOutput.tsx
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Output"
msgstr ""
#. js-lingui-id: xvfn7l
#: src/modules/workflow/workflow-steps/workflow-actions/ai-agent-action/components/WorkflowOutputSchemaBuilder.tsx
msgid "Output Field {fieldNumber}"
@ -7585,6 +7623,11 @@ msgstr "Dominio pubblico eliminato con successo"
msgid "Public Domains"
msgstr "Domini Pubblici"
#. js-lingui-id: GUpZQM
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Python Code Execution"
msgstr ""
#. js-lingui-id: 2vudsu
#: src/modules/command-menu/pages/page-layout/utils/getDateGranularityLabel.ts
msgid "Quarter"
@ -8029,6 +8072,11 @@ msgstr "Esegui un workflow e torna qui per visualizzare le esecuzioni"
msgid "Run Function"
msgstr "Esegui Funzione"
#. js-lingui-id: LTC198
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Running..."
msgstr ""
#. js-lingui-id: nji0/X
#: src/pages/settings/profile/appearance/components/LocalePicker.tsx
msgid "Russian"
@ -10194,6 +10242,11 @@ msgstr "In attesa"
msgid "Waiting Children"
msgstr "Bambini in attesa"
#. js-lingui-id: TdQd9Q
#: src/modules/ai/components/TerminalOutput.tsx
msgid "Waiting for output..."
msgstr ""
#. js-lingui-id: zFSQY3
#: src/modules/activities/timeline-activities/rows/main-object/components/EventRowMainObject.tsx
msgid "was created by"

View file

@ -2070,6 +2070,11 @@ msgstr "閉じる"
msgid "Close command menu"
msgstr "コマンドメニューを閉じる"
#. js-lingui-id: EWPtMO
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Code"
msgstr ""
#. js-lingui-id: /8PmQ9
#: src/modules/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowEditActionServerlessFunction.tsx
msgid "Code Editor"
@ -2147,6 +2152,7 @@ msgstr "完了"
#. js-lingui-id: qqWcBV
#: src/modules/settings/admin-panel/health-status/components/SettingsAdminQueueJobsTable.tsx
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Completed"
msgstr "完了"
@ -2397,6 +2403,11 @@ msgstr "クリップボードにコピーされました"
msgid "Copy"
msgstr "コピー"
#. js-lingui-id: NmPNJJ
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Copy code"
msgstr ""
#. js-lingui-id: 7eVkEH
#: src/pages/onboarding/InviteTeam.tsx
msgid "Copy invitation link"
@ -2412,6 +2423,11 @@ msgstr "リンクをコピー"
msgid "Copy link to view"
msgstr "\"ビューのリンクをコピー\""
#. js-lingui-id: VBIlvI
#: src/modules/ai/components/TerminalOutput.tsx
msgid "Copy output"
msgstr ""
#. js-lingui-id: HvB+os
#: src/pages/settings/SettingsTwoFactorAuthenticationMethod.tsx
msgid "Copy paste the code below"
@ -3452,6 +3468,11 @@ msgstr "ドットとコンマ - {dotsAndCommaExample}"
msgid "Downgrade"
msgstr "ダウングレード"
#. js-lingui-id: qnTwse
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Download {filename}"
msgstr ""
#. js-lingui-id: WcWS//
#: src/modules/settings/security/components/SSO/SettingsSSOSAMLForm.tsx
msgid "Download file"
@ -4352,6 +4373,7 @@ msgstr "追加クレジットが使用されました"
#. js-lingui-id: 7Bj3x9
#: src/pages/settings/emailing-domains/utils/getEmailingDomainStatusText.ts
#: src/modules/settings/admin-panel/health-status/components/SettingsAdminQueueJobsTable.tsx
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Failed"
msgstr "失敗"
@ -4714,6 +4736,11 @@ msgstr "一般"
msgid "General - Settings"
msgstr "一般 - 設定"
#. js-lingui-id: wu9eMO
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Generated Files"
msgstr ""
#. js-lingui-id: DDcvSo
#: src/pages/settings/profile/appearance/components/LocalePicker.tsx
msgid "German"
@ -6670,6 +6697,11 @@ msgstr "モデルが利用可能ではありません。ワークスペース設
msgid "No option found"
msgstr "オプションが見つかりません"
#. js-lingui-id: XRc1G9
#: src/modules/ai/components/TerminalOutput.tsx
msgid "No output"
msgstr ""
#. js-lingui-id: tTItk7
#: src/modules/workflow/workflow-steps/components/WorkflowRunStepOutputDetail.tsx
msgid "No output available"
@ -7140,6 +7172,12 @@ msgstr "その他のワークスペース"
msgid "Our team can help you set up your workspace to match your specific needs and workflows."
msgstr "チームがあなたの特定のニーズとワークフローに合わせてワークスペースをセットアップするお手伝いをします。"
#. js-lingui-id: gh06VD
#: src/modules/ai/components/TerminalOutput.tsx
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Output"
msgstr ""
#. js-lingui-id: xvfn7l
#: src/modules/workflow/workflow-steps/workflow-actions/ai-agent-action/components/WorkflowOutputSchemaBuilder.tsx
msgid "Output Field {fieldNumber}"
@ -7585,6 +7623,11 @@ msgstr "パブリックドメインが正常に削除されました"
msgid "Public Domains"
msgstr "パブリックドメイン"
#. js-lingui-id: GUpZQM
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Python Code Execution"
msgstr ""
#. js-lingui-id: 2vudsu
#: src/modules/command-menu/pages/page-layout/utils/getDateGranularityLabel.ts
msgid "Quarter"
@ -8029,6 +8072,11 @@ msgstr "ワークフローを実行し、その実行内容を見るためにこ
msgid "Run Function"
msgstr "関数を実行"
#. js-lingui-id: LTC198
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Running..."
msgstr ""
#. js-lingui-id: nji0/X
#: src/pages/settings/profile/appearance/components/LocalePicker.tsx
msgid "Russian"
@ -10192,6 +10240,11 @@ msgstr "待機中"
msgid "Waiting Children"
msgstr "待機中の子供"
#. js-lingui-id: TdQd9Q
#: src/modules/ai/components/TerminalOutput.tsx
msgid "Waiting for output..."
msgstr ""
#. js-lingui-id: zFSQY3
#: src/modules/activities/timeline-activities/rows/main-object/components/EventRowMainObject.tsx
msgid "was created by"

View file

@ -2070,6 +2070,11 @@ msgstr "닫기"
msgid "Close command menu"
msgstr "명령 메뉴 닫기"
#. js-lingui-id: EWPtMO
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Code"
msgstr ""
#. js-lingui-id: /8PmQ9
#: src/modules/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowEditActionServerlessFunction.tsx
msgid "Code Editor"
@ -2147,6 +2152,7 @@ msgstr "완료됨"
#. js-lingui-id: qqWcBV
#: src/modules/settings/admin-panel/health-status/components/SettingsAdminQueueJobsTable.tsx
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Completed"
msgstr "완료됨"
@ -2397,6 +2403,11 @@ msgstr "클립보드에 복사되었습니다"
msgid "Copy"
msgstr "복사"
#. js-lingui-id: NmPNJJ
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Copy code"
msgstr ""
#. js-lingui-id: 7eVkEH
#: src/pages/onboarding/InviteTeam.tsx
msgid "Copy invitation link"
@ -2412,6 +2423,11 @@ msgstr "링크 복사"
msgid "Copy link to view"
msgstr "보기 링크 복사"
#. js-lingui-id: VBIlvI
#: src/modules/ai/components/TerminalOutput.tsx
msgid "Copy output"
msgstr ""
#. js-lingui-id: HvB+os
#: src/pages/settings/SettingsTwoFactorAuthenticationMethod.tsx
msgid "Copy paste the code below"
@ -3452,6 +3468,11 @@ msgstr "점과 쉼표 - {dotsAndCommaExample}"
msgid "Downgrade"
msgstr "다운그레이드"
#. js-lingui-id: qnTwse
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Download {filename}"
msgstr ""
#. js-lingui-id: WcWS//
#: src/modules/settings/security/components/SSO/SettingsSSOSAMLForm.tsx
msgid "Download file"
@ -4352,6 +4373,7 @@ msgstr "추가 크레딧 사용됨"
#. js-lingui-id: 7Bj3x9
#: src/pages/settings/emailing-domains/utils/getEmailingDomainStatusText.ts
#: src/modules/settings/admin-panel/health-status/components/SettingsAdminQueueJobsTable.tsx
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Failed"
msgstr "실패"
@ -4714,6 +4736,11 @@ msgstr "일반"
msgid "General - Settings"
msgstr "일반 - 설정"
#. js-lingui-id: wu9eMO
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Generated Files"
msgstr ""
#. js-lingui-id: DDcvSo
#: src/pages/settings/profile/appearance/components/LocalePicker.tsx
msgid "German"
@ -6670,6 +6697,11 @@ msgstr "사용 가능한 모델이 없습니다. 작업 공간 설정에서 AI
msgid "No option found"
msgstr "옵션을 찾을 수 없음"
#. js-lingui-id: XRc1G9
#: src/modules/ai/components/TerminalOutput.tsx
msgid "No output"
msgstr ""
#. js-lingui-id: tTItk7
#: src/modules/workflow/workflow-steps/components/WorkflowRunStepOutputDetail.tsx
msgid "No output available"
@ -7140,6 +7172,12 @@ msgstr "기타 워크스페이스"
msgid "Our team can help you set up your workspace to match your specific needs and workflows."
msgstr "저희 팀은 귀하의 특정 요구와 워크플로우에 맞게 작업 공간을 설정하는 데 도움을 줄 수 있습니다."
#. js-lingui-id: gh06VD
#: src/modules/ai/components/TerminalOutput.tsx
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Output"
msgstr ""
#. js-lingui-id: xvfn7l
#: src/modules/workflow/workflow-steps/workflow-actions/ai-agent-action/components/WorkflowOutputSchemaBuilder.tsx
msgid "Output Field {fieldNumber}"
@ -7585,6 +7623,11 @@ msgstr "공개 도메인이 성공적으로 삭제되었습니다."
msgid "Public Domains"
msgstr "공개 도메인"
#. js-lingui-id: GUpZQM
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Python Code Execution"
msgstr ""
#. js-lingui-id: 2vudsu
#: src/modules/command-menu/pages/page-layout/utils/getDateGranularityLabel.ts
msgid "Quarter"
@ -8029,6 +8072,11 @@ msgstr "워크플로를 실행하고 여기로 돌아와 실행 결과를 확인
msgid "Run Function"
msgstr "기능 실행"
#. js-lingui-id: LTC198
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Running..."
msgstr ""
#. js-lingui-id: nji0/X
#: src/pages/settings/profile/appearance/components/LocalePicker.tsx
msgid "Russian"
@ -10192,6 +10240,11 @@ msgstr "대기 중"
msgid "Waiting Children"
msgstr "대기 중인 하위 항목"
#. js-lingui-id: TdQd9Q
#: src/modules/ai/components/TerminalOutput.tsx
msgid "Waiting for output..."
msgstr ""
#. js-lingui-id: zFSQY3
#: src/modules/activities/timeline-activities/rows/main-object/components/EventRowMainObject.tsx
msgid "was created by"

View file

@ -2070,6 +2070,11 @@ msgstr "Sluiten"
msgid "Close command menu"
msgstr "Sluit opdrachtmenu"
#. js-lingui-id: EWPtMO
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Code"
msgstr ""
#. js-lingui-id: /8PmQ9
#: src/modules/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowEditActionServerlessFunction.tsx
msgid "Code Editor"
@ -2147,6 +2152,7 @@ msgstr "voltooid"
#. js-lingui-id: qqWcBV
#: src/modules/settings/admin-panel/health-status/components/SettingsAdminQueueJobsTable.tsx
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Completed"
msgstr "Voltooid"
@ -2397,6 +2403,11 @@ msgstr "Gekopieerd naar klembord"
msgid "Copy"
msgstr "Kopiëren"
#. js-lingui-id: NmPNJJ
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Copy code"
msgstr ""
#. js-lingui-id: 7eVkEH
#: src/pages/onboarding/InviteTeam.tsx
msgid "Copy invitation link"
@ -2412,6 +2423,11 @@ msgstr "Kopieer link"
msgid "Copy link to view"
msgstr "Kopieer link om te bekijken"
#. js-lingui-id: VBIlvI
#: src/modules/ai/components/TerminalOutput.tsx
msgid "Copy output"
msgstr ""
#. js-lingui-id: HvB+os
#: src/pages/settings/SettingsTwoFactorAuthenticationMethod.tsx
msgid "Copy paste the code below"
@ -3452,6 +3468,11 @@ msgstr "Punten en komma - {dotsAndCommaExample}"
msgid "Downgrade"
msgstr "Downgrade"
#. js-lingui-id: qnTwse
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Download {filename}"
msgstr ""
#. js-lingui-id: WcWS//
#: src/modules/settings/security/components/SSO/SettingsSSOSAMLForm.tsx
msgid "Download file"
@ -4352,6 +4373,7 @@ msgstr "Extra credits gebruikt"
#. js-lingui-id: 7Bj3x9
#: src/pages/settings/emailing-domains/utils/getEmailingDomainStatusText.ts
#: src/modules/settings/admin-panel/health-status/components/SettingsAdminQueueJobsTable.tsx
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Failed"
msgstr "Mislukt"
@ -4714,6 +4736,11 @@ msgstr "Algemeen"
msgid "General - Settings"
msgstr "Algemeen - Instellingen"
#. js-lingui-id: wu9eMO
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Generated Files"
msgstr ""
#. js-lingui-id: DDcvSo
#: src/pages/settings/profile/appearance/components/LocalePicker.tsx
msgid "German"
@ -6670,6 +6697,11 @@ msgstr "Geen modellen beschikbaar. Configureer AI-modellen in je werkruimte-inst
msgid "No option found"
msgstr "Geen optie gevonden"
#. js-lingui-id: XRc1G9
#: src/modules/ai/components/TerminalOutput.tsx
msgid "No output"
msgstr ""
#. js-lingui-id: tTItk7
#: src/modules/workflow/workflow-steps/components/WorkflowRunStepOutputDetail.tsx
msgid "No output available"
@ -7140,6 +7172,12 @@ msgstr "Andere werkruimtes"
msgid "Our team can help you set up your workspace to match your specific needs and workflows."
msgstr "Ons team kan u helpen uw werkruimte in te richten, zodat deze aansluit bij uw specifieke behoeften en werkstromen."
#. js-lingui-id: gh06VD
#: src/modules/ai/components/TerminalOutput.tsx
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Output"
msgstr ""
#. js-lingui-id: xvfn7l
#: src/modules/workflow/workflow-steps/workflow-actions/ai-agent-action/components/WorkflowOutputSchemaBuilder.tsx
msgid "Output Field {fieldNumber}"
@ -7585,6 +7623,11 @@ msgstr "Publiek domein succesvol verwijderd"
msgid "Public Domains"
msgstr "Openbare Domeinen"
#. js-lingui-id: GUpZQM
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Python Code Execution"
msgstr ""
#. js-lingui-id: 2vudsu
#: src/modules/command-menu/pages/page-layout/utils/getDateGranularityLabel.ts
msgid "Quarter"
@ -8029,6 +8072,11 @@ msgstr "Voer een workflow uit en keer hier terug om de uitvoeringen te bekijken"
msgid "Run Function"
msgstr "Functie uitvoeren"
#. js-lingui-id: LTC198
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Running..."
msgstr ""
#. js-lingui-id: nji0/X
#: src/pages/settings/profile/appearance/components/LocalePicker.tsx
msgid "Russian"
@ -10194,6 +10242,11 @@ msgstr "Wachten"
msgid "Waiting Children"
msgstr "Wachten Kinderen"
#. js-lingui-id: TdQd9Q
#: src/modules/ai/components/TerminalOutput.tsx
msgid "Waiting for output..."
msgstr ""
#. js-lingui-id: zFSQY3
#: src/modules/activities/timeline-activities/rows/main-object/components/EventRowMainObject.tsx
msgid "was created by"

View file

@ -2070,6 +2070,11 @@ msgstr "Lukk"
msgid "Close command menu"
msgstr "Lukk kommandomeny"
#. js-lingui-id: EWPtMO
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Code"
msgstr ""
#. js-lingui-id: /8PmQ9
#: src/modules/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowEditActionServerlessFunction.tsx
msgid "Code Editor"
@ -2147,6 +2152,7 @@ msgstr "fullført"
#. js-lingui-id: qqWcBV
#: src/modules/settings/admin-panel/health-status/components/SettingsAdminQueueJobsTable.tsx
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Completed"
msgstr "Fullført"
@ -2397,6 +2403,11 @@ msgstr "Kopiert til utklippstavlen"
msgid "Copy"
msgstr "Kopier"
#. js-lingui-id: NmPNJJ
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Copy code"
msgstr ""
#. js-lingui-id: 7eVkEH
#: src/pages/onboarding/InviteTeam.tsx
msgid "Copy invitation link"
@ -2412,6 +2423,11 @@ msgstr "Kopier lenke"
msgid "Copy link to view"
msgstr "Kopier lenke for å vise"
#. js-lingui-id: VBIlvI
#: src/modules/ai/components/TerminalOutput.tsx
msgid "Copy output"
msgstr ""
#. js-lingui-id: HvB+os
#: src/pages/settings/SettingsTwoFactorAuthenticationMethod.tsx
msgid "Copy paste the code below"
@ -3452,6 +3468,11 @@ msgstr "Punktum og komma - {dotsAndCommaExample}"
msgid "Downgrade"
msgstr "Nedgrader"
#. js-lingui-id: qnTwse
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Download {filename}"
msgstr ""
#. js-lingui-id: WcWS//
#: src/modules/settings/security/components/SSO/SettingsSSOSAMLForm.tsx
msgid "Download file"
@ -4352,6 +4373,7 @@ msgstr "Ekstra kreditter brukt"
#. js-lingui-id: 7Bj3x9
#: src/pages/settings/emailing-domains/utils/getEmailingDomainStatusText.ts
#: src/modules/settings/admin-panel/health-status/components/SettingsAdminQueueJobsTable.tsx
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Failed"
msgstr "Mislyktes"
@ -4714,6 +4736,11 @@ msgstr "Generelt"
msgid "General - Settings"
msgstr "Generelt - Innstillinger"
#. js-lingui-id: wu9eMO
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Generated Files"
msgstr ""
#. js-lingui-id: DDcvSo
#: src/pages/settings/profile/appearance/components/LocalePicker.tsx
msgid "German"
@ -6670,6 +6697,11 @@ msgstr "Ingen modeller tilgjengelig. Vennligst konfigurer AI-modeller i arbeidso
msgid "No option found"
msgstr "Ingen alternativ funnet"
#. js-lingui-id: XRc1G9
#: src/modules/ai/components/TerminalOutput.tsx
msgid "No output"
msgstr ""
#. js-lingui-id: tTItk7
#: src/modules/workflow/workflow-steps/components/WorkflowRunStepOutputDetail.tsx
msgid "No output available"
@ -7140,6 +7172,12 @@ msgstr "Andre arbeidsområder"
msgid "Our team can help you set up your workspace to match your specific needs and workflows."
msgstr "Vårt team kan hjelpe deg med å sette opp arbeidsområdet ditt for å tilpasse dine spesifikke behov og arbeidsflyter."
#. js-lingui-id: gh06VD
#: src/modules/ai/components/TerminalOutput.tsx
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Output"
msgstr ""
#. js-lingui-id: xvfn7l
#: src/modules/workflow/workflow-steps/workflow-actions/ai-agent-action/components/WorkflowOutputSchemaBuilder.tsx
msgid "Output Field {fieldNumber}"
@ -7585,6 +7623,11 @@ msgstr "Offentlig domene slettet vellykket"
msgid "Public Domains"
msgstr "Offentlige domener"
#. js-lingui-id: GUpZQM
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Python Code Execution"
msgstr ""
#. js-lingui-id: 2vudsu
#: src/modules/command-menu/pages/page-layout/utils/getDateGranularityLabel.ts
msgid "Quarter"
@ -8029,6 +8072,11 @@ msgstr "Kjør en arbeidsflyt og kom tilbake her for å se på kjøringene"
msgid "Run Function"
msgstr "Kjør funksjon"
#. js-lingui-id: LTC198
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Running..."
msgstr ""
#. js-lingui-id: nji0/X
#: src/pages/settings/profile/appearance/components/LocalePicker.tsx
msgid "Russian"
@ -10192,6 +10240,11 @@ msgstr "Venter"
msgid "Waiting Children"
msgstr "Ventende barn"
#. js-lingui-id: TdQd9Q
#: src/modules/ai/components/TerminalOutput.tsx
msgid "Waiting for output..."
msgstr ""
#. js-lingui-id: zFSQY3
#: src/modules/activities/timeline-activities/rows/main-object/components/EventRowMainObject.tsx
msgid "was created by"

View file

@ -2070,6 +2070,11 @@ msgstr "Zamknij"
msgid "Close command menu"
msgstr "Zamknij menu poleceń"
#. js-lingui-id: EWPtMO
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Code"
msgstr ""
#. js-lingui-id: /8PmQ9
#: src/modules/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowEditActionServerlessFunction.tsx
msgid "Code Editor"
@ -2147,6 +2152,7 @@ msgstr "zakończono"
#. js-lingui-id: qqWcBV
#: src/modules/settings/admin-panel/health-status/components/SettingsAdminQueueJobsTable.tsx
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Completed"
msgstr "Zakończono"
@ -2397,6 +2403,11 @@ msgstr "Skopiowano do schowka"
msgid "Copy"
msgstr "Kopiuj"
#. js-lingui-id: NmPNJJ
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Copy code"
msgstr ""
#. js-lingui-id: 7eVkEH
#: src/pages/onboarding/InviteTeam.tsx
msgid "Copy invitation link"
@ -2412,6 +2423,11 @@ msgstr "Skopiuj link"
msgid "Copy link to view"
msgstr "Skopiuj link do wyświetlenia"
#. js-lingui-id: VBIlvI
#: src/modules/ai/components/TerminalOutput.tsx
msgid "Copy output"
msgstr ""
#. js-lingui-id: HvB+os
#: src/pages/settings/SettingsTwoFactorAuthenticationMethod.tsx
msgid "Copy paste the code below"
@ -3452,6 +3468,11 @@ msgstr "Kropki i przecinek - {dotsAndCommaExample}"
msgid "Downgrade"
msgstr "Zniż"
#. js-lingui-id: qnTwse
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Download {filename}"
msgstr ""
#. js-lingui-id: WcWS//
#: src/modules/settings/security/components/SSO/SettingsSSOSAMLForm.tsx
msgid "Download file"
@ -4352,6 +4373,7 @@ msgstr "Wykorzystane dodatkowe kredyty"
#. js-lingui-id: 7Bj3x9
#: src/pages/settings/emailing-domains/utils/getEmailingDomainStatusText.ts
#: src/modules/settings/admin-panel/health-status/components/SettingsAdminQueueJobsTable.tsx
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Failed"
msgstr "Niepowodzenie"
@ -4714,6 +4736,11 @@ msgstr "Ogólne"
msgid "General - Settings"
msgstr "Ogólne - Ustawienia"
#. js-lingui-id: wu9eMO
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Generated Files"
msgstr ""
#. js-lingui-id: DDcvSo
#: src/pages/settings/profile/appearance/components/LocalePicker.tsx
msgid "German"
@ -6670,6 +6697,11 @@ msgstr "Brak dostępnych modeli. Skonfiguruj modele AI w ustawieniach przestrzen
msgid "No option found"
msgstr "Nie znaleziono opcji"
#. js-lingui-id: XRc1G9
#: src/modules/ai/components/TerminalOutput.tsx
msgid "No output"
msgstr ""
#. js-lingui-id: tTItk7
#: src/modules/workflow/workflow-steps/components/WorkflowRunStepOutputDetail.tsx
msgid "No output available"
@ -7140,6 +7172,12 @@ msgstr "Inne przestrzenie robocze"
msgid "Our team can help you set up your workspace to match your specific needs and workflows."
msgstr "Nasz zespół może pomóc Ci skonfigurować przestrzeń roboczą, aby spełniała Twoje specyficzne potrzeby i procesy pracy."
#. js-lingui-id: gh06VD
#: src/modules/ai/components/TerminalOutput.tsx
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Output"
msgstr ""
#. js-lingui-id: xvfn7l
#: src/modules/workflow/workflow-steps/workflow-actions/ai-agent-action/components/WorkflowOutputSchemaBuilder.tsx
msgid "Output Field {fieldNumber}"
@ -7585,6 +7623,11 @@ msgstr "Domena publiczna pomyślnie usunięta"
msgid "Public Domains"
msgstr "Domeny publiczne"
#. js-lingui-id: GUpZQM
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Python Code Execution"
msgstr ""
#. js-lingui-id: 2vudsu
#: src/modules/command-menu/pages/page-layout/utils/getDateGranularityLabel.ts
msgid "Quarter"
@ -8029,6 +8072,11 @@ msgstr "Uruchom workflow i wróć tutaj, aby zobaczyć jego wykonania"
msgid "Run Function"
msgstr "Uruchom funkcję"
#. js-lingui-id: LTC198
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Running..."
msgstr ""
#. js-lingui-id: nji0/X
#: src/pages/settings/profile/appearance/components/LocalePicker.tsx
msgid "Russian"
@ -10192,6 +10240,11 @@ msgstr "Czekający"
msgid "Waiting Children"
msgstr "Czekające dzieci"
#. js-lingui-id: TdQd9Q
#: src/modules/ai/components/TerminalOutput.tsx
msgid "Waiting for output..."
msgstr ""
#. js-lingui-id: zFSQY3
#: src/modules/activities/timeline-activities/rows/main-object/components/EventRowMainObject.tsx
msgid "was created by"

View file

@ -2065,6 +2065,11 @@ msgstr ""
msgid "Close command menu"
msgstr ""
#. js-lingui-id: EWPtMO
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Code"
msgstr ""
#. js-lingui-id: /8PmQ9
#: src/modules/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowEditActionServerlessFunction.tsx
msgid "Code Editor"
@ -2142,6 +2147,7 @@ msgstr ""
#. js-lingui-id: qqWcBV
#: src/modules/settings/admin-panel/health-status/components/SettingsAdminQueueJobsTable.tsx
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Completed"
msgstr ""
@ -2392,6 +2398,11 @@ msgstr ""
msgid "Copy"
msgstr ""
#. js-lingui-id: NmPNJJ
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Copy code"
msgstr ""
#. js-lingui-id: 7eVkEH
#: src/pages/onboarding/InviteTeam.tsx
msgid "Copy invitation link"
@ -2407,6 +2418,11 @@ msgstr ""
msgid "Copy link to view"
msgstr ""
#. js-lingui-id: VBIlvI
#: src/modules/ai/components/TerminalOutput.tsx
msgid "Copy output"
msgstr ""
#. js-lingui-id: HvB+os
#: src/pages/settings/SettingsTwoFactorAuthenticationMethod.tsx
msgid "Copy paste the code below"
@ -3447,6 +3463,11 @@ msgstr ""
msgid "Downgrade"
msgstr ""
#. js-lingui-id: qnTwse
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Download {filename}"
msgstr ""
#. js-lingui-id: WcWS//
#: src/modules/settings/security/components/SSO/SettingsSSOSAMLForm.tsx
msgid "Download file"
@ -4347,6 +4368,7 @@ msgstr ""
#. js-lingui-id: 7Bj3x9
#: src/pages/settings/emailing-domains/utils/getEmailingDomainStatusText.ts
#: src/modules/settings/admin-panel/health-status/components/SettingsAdminQueueJobsTable.tsx
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Failed"
msgstr ""
@ -4709,6 +4731,11 @@ msgstr ""
msgid "General - Settings"
msgstr ""
#. js-lingui-id: wu9eMO
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Generated Files"
msgstr ""
#. js-lingui-id: DDcvSo
#: src/pages/settings/profile/appearance/components/LocalePicker.tsx
msgid "German"
@ -6665,6 +6692,11 @@ msgstr ""
msgid "No option found"
msgstr ""
#. js-lingui-id: XRc1G9
#: src/modules/ai/components/TerminalOutput.tsx
msgid "No output"
msgstr ""
#. js-lingui-id: tTItk7
#: src/modules/workflow/workflow-steps/components/WorkflowRunStepOutputDetail.tsx
msgid "No output available"
@ -7135,6 +7167,12 @@ msgstr ""
msgid "Our team can help you set up your workspace to match your specific needs and workflows."
msgstr ""
#. js-lingui-id: gh06VD
#: src/modules/ai/components/TerminalOutput.tsx
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Output"
msgstr ""
#. js-lingui-id: xvfn7l
#: src/modules/workflow/workflow-steps/workflow-actions/ai-agent-action/components/WorkflowOutputSchemaBuilder.tsx
msgid "Output Field {fieldNumber}"
@ -7580,6 +7618,11 @@ msgstr ""
msgid "Public Domains"
msgstr ""
#. js-lingui-id: GUpZQM
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Python Code Execution"
msgstr ""
#. js-lingui-id: 2vudsu
#: src/modules/command-menu/pages/page-layout/utils/getDateGranularityLabel.ts
msgid "Quarter"
@ -8024,6 +8067,11 @@ msgstr ""
msgid "Run Function"
msgstr ""
#. js-lingui-id: LTC198
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Running..."
msgstr ""
#. js-lingui-id: nji0/X
#: src/pages/settings/profile/appearance/components/LocalePicker.tsx
msgid "Russian"
@ -10187,6 +10235,11 @@ msgstr ""
msgid "Waiting Children"
msgstr ""
#. js-lingui-id: TdQd9Q
#: src/modules/ai/components/TerminalOutput.tsx
msgid "Waiting for output..."
msgstr ""
#. js-lingui-id: zFSQY3
#: src/modules/activities/timeline-activities/rows/main-object/components/EventRowMainObject.tsx
msgid "was created by"

View file

@ -2070,6 +2070,11 @@ msgstr "Fechar"
msgid "Close command menu"
msgstr "Fechar menu de comandos"
#. js-lingui-id: EWPtMO
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Code"
msgstr ""
#. js-lingui-id: /8PmQ9
#: src/modules/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowEditActionServerlessFunction.tsx
msgid "Code Editor"
@ -2147,6 +2152,7 @@ msgstr "completado"
#. js-lingui-id: qqWcBV
#: src/modules/settings/admin-panel/health-status/components/SettingsAdminQueueJobsTable.tsx
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Completed"
msgstr "Completado"
@ -2397,6 +2403,11 @@ msgstr "Copiado para a área de transferência"
msgid "Copy"
msgstr "Copiar"
#. js-lingui-id: NmPNJJ
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Copy code"
msgstr ""
#. js-lingui-id: 7eVkEH
#: src/pages/onboarding/InviteTeam.tsx
msgid "Copy invitation link"
@ -2412,6 +2423,11 @@ msgstr "Copiar link"
msgid "Copy link to view"
msgstr "Copiar link para visualizar"
#. js-lingui-id: VBIlvI
#: src/modules/ai/components/TerminalOutput.tsx
msgid "Copy output"
msgstr ""
#. js-lingui-id: HvB+os
#: src/pages/settings/SettingsTwoFactorAuthenticationMethod.tsx
msgid "Copy paste the code below"
@ -3452,6 +3468,11 @@ msgstr "Pontos e vírgula - {dotsAndCommaExample}"
msgid "Downgrade"
msgstr "Downgrade"
#. js-lingui-id: qnTwse
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Download {filename}"
msgstr ""
#. js-lingui-id: WcWS//
#: src/modules/settings/security/components/SSO/SettingsSSOSAMLForm.tsx
msgid "Download file"
@ -4352,6 +4373,7 @@ msgstr "Créditos extras utilizados"
#. js-lingui-id: 7Bj3x9
#: src/pages/settings/emailing-domains/utils/getEmailingDomainStatusText.ts
#: src/modules/settings/admin-panel/health-status/components/SettingsAdminQueueJobsTable.tsx
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Failed"
msgstr "Falhou"
@ -4714,6 +4736,11 @@ msgstr "Geral"
msgid "General - Settings"
msgstr "Geral - Configurações"
#. js-lingui-id: wu9eMO
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Generated Files"
msgstr ""
#. js-lingui-id: DDcvSo
#: src/pages/settings/profile/appearance/components/LocalePicker.tsx
msgid "German"
@ -6670,6 +6697,11 @@ msgstr "Nenhum modelo disponível. Por favor, configure modelos de IA nas config
msgid "No option found"
msgstr "Nenhuma opção encontrada"
#. js-lingui-id: XRc1G9
#: src/modules/ai/components/TerminalOutput.tsx
msgid "No output"
msgstr ""
#. js-lingui-id: tTItk7
#: src/modules/workflow/workflow-steps/components/WorkflowRunStepOutputDetail.tsx
msgid "No output available"
@ -7140,6 +7172,12 @@ msgstr "Outros espaços de trabalho"
msgid "Our team can help you set up your workspace to match your specific needs and workflows."
msgstr "Nossa equipe pode ajudar você a configurar seu espaço de trabalho para atender às suas necessidades e fluxos de trabalho específicos."
#. js-lingui-id: gh06VD
#: src/modules/ai/components/TerminalOutput.tsx
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Output"
msgstr ""
#. js-lingui-id: xvfn7l
#: src/modules/workflow/workflow-steps/workflow-actions/ai-agent-action/components/WorkflowOutputSchemaBuilder.tsx
msgid "Output Field {fieldNumber}"
@ -7585,6 +7623,11 @@ msgstr "Domínio público excluído com sucesso"
msgid "Public Domains"
msgstr "Domínios Públicos"
#. js-lingui-id: GUpZQM
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Python Code Execution"
msgstr ""
#. js-lingui-id: 2vudsu
#: src/modules/command-menu/pages/page-layout/utils/getDateGranularityLabel.ts
msgid "Quarter"
@ -8029,6 +8072,11 @@ msgstr "Execute um workflow e retorne aqui para visualizar suas execuções"
msgid "Run Function"
msgstr "Executar Função"
#. js-lingui-id: LTC198
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Running..."
msgstr ""
#. js-lingui-id: nji0/X
#: src/pages/settings/profile/appearance/components/LocalePicker.tsx
msgid "Russian"
@ -10192,6 +10240,11 @@ msgstr "Esperando"
msgid "Waiting Children"
msgstr "Crianças à Espera"
#. js-lingui-id: TdQd9Q
#: src/modules/ai/components/TerminalOutput.tsx
msgid "Waiting for output..."
msgstr ""
#. js-lingui-id: zFSQY3
#: src/modules/activities/timeline-activities/rows/main-object/components/EventRowMainObject.tsx
msgid "was created by"

View file

@ -2070,6 +2070,11 @@ msgstr "Fechar"
msgid "Close command menu"
msgstr "Fechar menu de comandos"
#. js-lingui-id: EWPtMO
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Code"
msgstr ""
#. js-lingui-id: /8PmQ9
#: src/modules/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowEditActionServerlessFunction.tsx
msgid "Code Editor"
@ -2147,6 +2152,7 @@ msgstr "completado"
#. js-lingui-id: qqWcBV
#: src/modules/settings/admin-panel/health-status/components/SettingsAdminQueueJobsTable.tsx
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Completed"
msgstr "Concluído"
@ -2397,6 +2403,11 @@ msgstr "Copiado para a área de transferência"
msgid "Copy"
msgstr "Copiar"
#. js-lingui-id: NmPNJJ
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Copy code"
msgstr ""
#. js-lingui-id: 7eVkEH
#: src/pages/onboarding/InviteTeam.tsx
msgid "Copy invitation link"
@ -2412,6 +2423,11 @@ msgstr "Copiar link"
msgid "Copy link to view"
msgstr "Copiar link para visualizar"
#. js-lingui-id: VBIlvI
#: src/modules/ai/components/TerminalOutput.tsx
msgid "Copy output"
msgstr ""
#. js-lingui-id: HvB+os
#: src/pages/settings/SettingsTwoFactorAuthenticationMethod.tsx
msgid "Copy paste the code below"
@ -3452,6 +3468,11 @@ msgstr "Pontos e vírgula - {dotsAndCommaExample}"
msgid "Downgrade"
msgstr "Downgrade"
#. js-lingui-id: qnTwse
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Download {filename}"
msgstr ""
#. js-lingui-id: WcWS//
#: src/modules/settings/security/components/SSO/SettingsSSOSAMLForm.tsx
msgid "Download file"
@ -4352,6 +4373,7 @@ msgstr "Créditos Extras Utilizados"
#. js-lingui-id: 7Bj3x9
#: src/pages/settings/emailing-domains/utils/getEmailingDomainStatusText.ts
#: src/modules/settings/admin-panel/health-status/components/SettingsAdminQueueJobsTable.tsx
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Failed"
msgstr "Falhou"
@ -4714,6 +4736,11 @@ msgstr "Geral"
msgid "General - Settings"
msgstr "Geral - Configurações"
#. js-lingui-id: wu9eMO
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Generated Files"
msgstr ""
#. js-lingui-id: DDcvSo
#: src/pages/settings/profile/appearance/components/LocalePicker.tsx
msgid "German"
@ -6670,6 +6697,11 @@ msgstr "Nenhum modelo disponível. Por favor, configure modelos de IA nas config
msgid "No option found"
msgstr "Nenhuma opção encontrada"
#. js-lingui-id: XRc1G9
#: src/modules/ai/components/TerminalOutput.tsx
msgid "No output"
msgstr ""
#. js-lingui-id: tTItk7
#: src/modules/workflow/workflow-steps/components/WorkflowRunStepOutputDetail.tsx
msgid "No output available"
@ -7140,6 +7172,12 @@ msgstr "Outros espaços de trabalho"
msgid "Our team can help you set up your workspace to match your specific needs and workflows."
msgstr "Nossa equipe pode ajudar a configurar seu espaço de trabalho de acordo com suas necessidades específicas e fluxos de trabalho."
#. js-lingui-id: gh06VD
#: src/modules/ai/components/TerminalOutput.tsx
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Output"
msgstr ""
#. js-lingui-id: xvfn7l
#: src/modules/workflow/workflow-steps/workflow-actions/ai-agent-action/components/WorkflowOutputSchemaBuilder.tsx
msgid "Output Field {fieldNumber}"
@ -7585,6 +7623,11 @@ msgstr "Domínio público excluído com sucesso"
msgid "Public Domains"
msgstr "Domínios Públicos"
#. js-lingui-id: GUpZQM
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Python Code Execution"
msgstr ""
#. js-lingui-id: 2vudsu
#: src/modules/command-menu/pages/page-layout/utils/getDateGranularityLabel.ts
msgid "Quarter"
@ -8029,6 +8072,11 @@ msgstr "Execute um workflow e volte aqui para ver as suas execuções"
msgid "Run Function"
msgstr "Executar Função"
#. js-lingui-id: LTC198
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Running..."
msgstr ""
#. js-lingui-id: nji0/X
#: src/pages/settings/profile/appearance/components/LocalePicker.tsx
msgid "Russian"
@ -10192,6 +10240,11 @@ msgstr "Aguardando"
msgid "Waiting Children"
msgstr "Crianças esperando"
#. js-lingui-id: TdQd9Q
#: src/modules/ai/components/TerminalOutput.tsx
msgid "Waiting for output..."
msgstr ""
#. js-lingui-id: zFSQY3
#: src/modules/activities/timeline-activities/rows/main-object/components/EventRowMainObject.tsx
msgid "was created by"

View file

@ -2070,6 +2070,11 @@ msgstr "Închide"
msgid "Close command menu"
msgstr "Închide meniul de comandă"
#. js-lingui-id: EWPtMO
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Code"
msgstr ""
#. js-lingui-id: /8PmQ9
#: src/modules/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowEditActionServerlessFunction.tsx
msgid "Code Editor"
@ -2147,6 +2152,7 @@ msgstr "completat"
#. js-lingui-id: qqWcBV
#: src/modules/settings/admin-panel/health-status/components/SettingsAdminQueueJobsTable.tsx
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Completed"
msgstr "Completat"
@ -2397,6 +2403,11 @@ msgstr "Copiat în clipboard"
msgid "Copy"
msgstr "Copiază"
#. js-lingui-id: NmPNJJ
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Copy code"
msgstr ""
#. js-lingui-id: 7eVkEH
#: src/pages/onboarding/InviteTeam.tsx
msgid "Copy invitation link"
@ -2412,6 +2423,11 @@ msgstr "Copiază linkul"
msgid "Copy link to view"
msgstr "Copiază linkul pentru vizualizare"
#. js-lingui-id: VBIlvI
#: src/modules/ai/components/TerminalOutput.tsx
msgid "Copy output"
msgstr ""
#. js-lingui-id: HvB+os
#: src/pages/settings/SettingsTwoFactorAuthenticationMethod.tsx
msgid "Copy paste the code below"
@ -3452,6 +3468,11 @@ msgstr "Puncte și virgulă - {dotsAndCommaExample}"
msgid "Downgrade"
msgstr "Retrogradare"
#. js-lingui-id: qnTwse
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Download {filename}"
msgstr ""
#. js-lingui-id: WcWS//
#: src/modules/settings/security/components/SSO/SettingsSSOSAMLForm.tsx
msgid "Download file"
@ -4352,6 +4373,7 @@ msgstr "Credite suplimentare utilizate"
#. js-lingui-id: 7Bj3x9
#: src/pages/settings/emailing-domains/utils/getEmailingDomainStatusText.ts
#: src/modules/settings/admin-panel/health-status/components/SettingsAdminQueueJobsTable.tsx
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Failed"
msgstr "Eșuat"
@ -4714,6 +4736,11 @@ msgstr "General"
msgid "General - Settings"
msgstr "General - Setări"
#. js-lingui-id: wu9eMO
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Generated Files"
msgstr ""
#. js-lingui-id: DDcvSo
#: src/pages/settings/profile/appearance/components/LocalePicker.tsx
msgid "German"
@ -6670,6 +6697,11 @@ msgstr "Nu există modele disponibile. Vă rugăm să configurați modelele AI
msgid "No option found"
msgstr "Nu a fost găsită nicio opțiune"
#. js-lingui-id: XRc1G9
#: src/modules/ai/components/TerminalOutput.tsx
msgid "No output"
msgstr ""
#. js-lingui-id: tTItk7
#: src/modules/workflow/workflow-steps/components/WorkflowRunStepOutputDetail.tsx
msgid "No output available"
@ -7140,6 +7172,12 @@ msgstr "Alte spații de lucru"
msgid "Our team can help you set up your workspace to match your specific needs and workflows."
msgstr "Echipa noastră vă poate ajuta să vă configurați spațiul de lucru pentru a se potrivi nevoilor și fluxurilor dvs. de lucru specifice."
#. js-lingui-id: gh06VD
#: src/modules/ai/components/TerminalOutput.tsx
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Output"
msgstr ""
#. js-lingui-id: xvfn7l
#: src/modules/workflow/workflow-steps/workflow-actions/ai-agent-action/components/WorkflowOutputSchemaBuilder.tsx
msgid "Output Field {fieldNumber}"
@ -7585,6 +7623,11 @@ msgstr "Domeniul public a fost șters cu succes"
msgid "Public Domains"
msgstr "Domenii Publice"
#. js-lingui-id: GUpZQM
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Python Code Execution"
msgstr ""
#. js-lingui-id: 2vudsu
#: src/modules/command-menu/pages/page-layout/utils/getDateGranularityLabel.ts
msgid "Quarter"
@ -8029,6 +8072,11 @@ msgstr "Rulează un workflow și întoarce-te aici pentru a vedea execuțiile ac
msgid "Run Function"
msgstr "Execută Funcția"
#. js-lingui-id: LTC198
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Running..."
msgstr ""
#. js-lingui-id: nji0/X
#: src/pages/settings/profile/appearance/components/LocalePicker.tsx
msgid "Russian"
@ -10192,6 +10240,11 @@ msgstr "În așteptare"
msgid "Waiting Children"
msgstr "Copii în așteptare"
#. js-lingui-id: TdQd9Q
#: src/modules/ai/components/TerminalOutput.tsx
msgid "Waiting for output..."
msgstr ""
#. js-lingui-id: zFSQY3
#: src/modules/activities/timeline-activities/rows/main-object/components/EventRowMainObject.tsx
msgid "was created by"

View file

@ -2070,6 +2070,11 @@ msgstr "Затвори"
msgid "Close command menu"
msgstr "Затвори мени команди"
#. js-lingui-id: EWPtMO
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Code"
msgstr ""
#. js-lingui-id: /8PmQ9
#: src/modules/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowEditActionServerlessFunction.tsx
msgid "Code Editor"
@ -2147,6 +2152,7 @@ msgstr "завршено"
#. js-lingui-id: qqWcBV
#: src/modules/settings/admin-panel/health-status/components/SettingsAdminQueueJobsTable.tsx
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Completed"
msgstr "Завршено"
@ -2397,6 +2403,11 @@ msgstr "Копирано у клипборд"
msgid "Copy"
msgstr "Копирај"
#. js-lingui-id: NmPNJJ
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Copy code"
msgstr ""
#. js-lingui-id: 7eVkEH
#: src/pages/onboarding/InviteTeam.tsx
msgid "Copy invitation link"
@ -2412,6 +2423,11 @@ msgstr "Копирај линк"
msgid "Copy link to view"
msgstr "Копирај везу за преглед"
#. js-lingui-id: VBIlvI
#: src/modules/ai/components/TerminalOutput.tsx
msgid "Copy output"
msgstr ""
#. js-lingui-id: HvB+os
#: src/pages/settings/SettingsTwoFactorAuthenticationMethod.tsx
msgid "Copy paste the code below"
@ -3452,6 +3468,11 @@ msgstr "Тачке и запета - {dotsAndCommaExample}"
msgid "Downgrade"
msgstr "Смањи услугу"
#. js-lingui-id: qnTwse
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Download {filename}"
msgstr ""
#. js-lingui-id: WcWS//
#: src/modules/settings/security/components/SSO/SettingsSSOSAMLForm.tsx
msgid "Download file"
@ -4352,6 +4373,7 @@ msgstr "Искоришћени додатни кредити"
#. js-lingui-id: 7Bj3x9
#: src/pages/settings/emailing-domains/utils/getEmailingDomainStatusText.ts
#: src/modules/settings/admin-panel/health-status/components/SettingsAdminQueueJobsTable.tsx
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Failed"
msgstr "Неуспело"
@ -4714,6 +4736,11 @@ msgstr "Опште"
msgid "General - Settings"
msgstr "Опште - Подешавања"
#. js-lingui-id: wu9eMO
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Generated Files"
msgstr ""
#. js-lingui-id: DDcvSo
#: src/pages/settings/profile/appearance/components/LocalePicker.tsx
msgid "German"
@ -6670,6 +6697,11 @@ msgstr "Нема доступних модела. Молимо вас, конф
msgid "No option found"
msgstr "Нема пронађене опције"
#. js-lingui-id: XRc1G9
#: src/modules/ai/components/TerminalOutput.tsx
msgid "No output"
msgstr ""
#. js-lingui-id: tTItk7
#: src/modules/workflow/workflow-steps/components/WorkflowRunStepOutputDetail.tsx
msgid "No output available"
@ -7140,6 +7172,12 @@ msgstr "Остали радни простори"
msgid "Our team can help you set up your workspace to match your specific needs and workflows."
msgstr "Наш тим може помоћи у подешавању вашег радног простора тако да одговара вашим специфичним потребама и радним токовима."
#. js-lingui-id: gh06VD
#: src/modules/ai/components/TerminalOutput.tsx
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Output"
msgstr ""
#. js-lingui-id: xvfn7l
#: src/modules/workflow/workflow-steps/workflow-actions/ai-agent-action/components/WorkflowOutputSchemaBuilder.tsx
msgid "Output Field {fieldNumber}"
@ -7585,6 +7623,11 @@ msgstr "Јавни домен успешно обрисан"
msgid "Public Domains"
msgstr "Јавни домени"
#. js-lingui-id: GUpZQM
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Python Code Execution"
msgstr ""
#. js-lingui-id: 2vudsu
#: src/modules/command-menu/pages/page-layout/utils/getDateGranularityLabel.ts
msgid "Quarter"
@ -8029,6 +8072,11 @@ msgstr "Покрените радни процес и вратите се овд
msgid "Run Function"
msgstr "Изврши функцију"
#. js-lingui-id: LTC198
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Running..."
msgstr ""
#. js-lingui-id: nji0/X
#: src/pages/settings/profile/appearance/components/LocalePicker.tsx
msgid "Russian"
@ -10192,6 +10240,11 @@ msgstr "Чекање"
msgid "Waiting Children"
msgstr "Чекање деце"
#. js-lingui-id: TdQd9Q
#: src/modules/ai/components/TerminalOutput.tsx
msgid "Waiting for output..."
msgstr ""
#. js-lingui-id: zFSQY3
#: src/modules/activities/timeline-activities/rows/main-object/components/EventRowMainObject.tsx
msgid "was created by"

View file

@ -2070,6 +2070,11 @@ msgstr "Stäng"
msgid "Close command menu"
msgstr "Stäng kommandomenyn"
#. js-lingui-id: EWPtMO
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Code"
msgstr ""
#. js-lingui-id: /8PmQ9
#: src/modules/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowEditActionServerlessFunction.tsx
msgid "Code Editor"
@ -2147,6 +2152,7 @@ msgstr "slutförd"
#. js-lingui-id: qqWcBV
#: src/modules/settings/admin-panel/health-status/components/SettingsAdminQueueJobsTable.tsx
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Completed"
msgstr "Slutförd"
@ -2397,6 +2403,11 @@ msgstr "Kopierad till urklipp"
msgid "Copy"
msgstr "Kopiera"
#. js-lingui-id: NmPNJJ
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Copy code"
msgstr ""
#. js-lingui-id: 7eVkEH
#: src/pages/onboarding/InviteTeam.tsx
msgid "Copy invitation link"
@ -2412,6 +2423,11 @@ msgstr "Kopiera länk"
msgid "Copy link to view"
msgstr "Kopiera länk för att se"
#. js-lingui-id: VBIlvI
#: src/modules/ai/components/TerminalOutput.tsx
msgid "Copy output"
msgstr ""
#. js-lingui-id: HvB+os
#: src/pages/settings/SettingsTwoFactorAuthenticationMethod.tsx
msgid "Copy paste the code below"
@ -3452,6 +3468,11 @@ msgstr "Punkt och komma - {dotsAndCommaExample}"
msgid "Downgrade"
msgstr "Nedgradera"
#. js-lingui-id: qnTwse
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Download {filename}"
msgstr ""
#. js-lingui-id: WcWS//
#: src/modules/settings/security/components/SSO/SettingsSSOSAMLForm.tsx
msgid "Download file"
@ -4352,6 +4373,7 @@ msgstr "Extra krediter användes"
#. js-lingui-id: 7Bj3x9
#: src/pages/settings/emailing-domains/utils/getEmailingDomainStatusText.ts
#: src/modules/settings/admin-panel/health-status/components/SettingsAdminQueueJobsTable.tsx
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Failed"
msgstr "Misslyckades"
@ -4714,6 +4736,11 @@ msgstr "Allmänt"
msgid "General - Settings"
msgstr "Allmänt - Inställningar"
#. js-lingui-id: wu9eMO
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Generated Files"
msgstr ""
#. js-lingui-id: DDcvSo
#: src/pages/settings/profile/appearance/components/LocalePicker.tsx
msgid "German"
@ -6672,6 +6699,11 @@ msgstr "Inga modeller tillgängliga. Vänligen konfigurera AI-modeller i dina ar
msgid "No option found"
msgstr "Inget alternativ hittades"
#. js-lingui-id: XRc1G9
#: src/modules/ai/components/TerminalOutput.tsx
msgid "No output"
msgstr ""
#. js-lingui-id: tTItk7
#: src/modules/workflow/workflow-steps/components/WorkflowRunStepOutputDetail.tsx
msgid "No output available"
@ -7142,6 +7174,12 @@ msgstr "Andra arbetsytor"
msgid "Our team can help you set up your workspace to match your specific needs and workflows."
msgstr "Vårt team kan hjälpa dig att ställa in din arbetsyta så att den matchar dina specifika behov och arbetsflöden."
#. js-lingui-id: gh06VD
#: src/modules/ai/components/TerminalOutput.tsx
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Output"
msgstr ""
#. js-lingui-id: xvfn7l
#: src/modules/workflow/workflow-steps/workflow-actions/ai-agent-action/components/WorkflowOutputSchemaBuilder.tsx
msgid "Output Field {fieldNumber}"
@ -7587,6 +7625,11 @@ msgstr "Allmän domän raderades framgångsrikt"
msgid "Public Domains"
msgstr "Allmänna domäner"
#. js-lingui-id: GUpZQM
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Python Code Execution"
msgstr ""
#. js-lingui-id: 2vudsu
#: src/modules/command-menu/pages/page-layout/utils/getDateGranularityLabel.ts
msgid "Quarter"
@ -8031,6 +8074,11 @@ msgstr "Kör en arbetsflöde och återvänd hit för att se dess körningar"
msgid "Run Function"
msgstr "Kör Funktion"
#. js-lingui-id: LTC198
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Running..."
msgstr ""
#. js-lingui-id: nji0/X
#: src/pages/settings/profile/appearance/components/LocalePicker.tsx
msgid "Russian"
@ -10206,6 +10254,11 @@ msgstr "Väntar"
msgid "Waiting Children"
msgstr "Väntar barn"
#. js-lingui-id: TdQd9Q
#: src/modules/ai/components/TerminalOutput.tsx
msgid "Waiting for output..."
msgstr ""
#. js-lingui-id: zFSQY3
#: src/modules/activities/timeline-activities/rows/main-object/components/EventRowMainObject.tsx
msgid "was created by"

View file

@ -2070,6 +2070,11 @@ msgstr "Kapat"
msgid "Close command menu"
msgstr "Komut menüsünü kapat"
#. js-lingui-id: EWPtMO
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Code"
msgstr ""
#. js-lingui-id: /8PmQ9
#: src/modules/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowEditActionServerlessFunction.tsx
msgid "Code Editor"
@ -2147,6 +2152,7 @@ msgstr "tamamlandı"
#. js-lingui-id: qqWcBV
#: src/modules/settings/admin-panel/health-status/components/SettingsAdminQueueJobsTable.tsx
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Completed"
msgstr "Tamamlandı"
@ -2397,6 +2403,11 @@ msgstr "Panoya kopyalandı"
msgid "Copy"
msgstr "Kopyala"
#. js-lingui-id: NmPNJJ
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Copy code"
msgstr ""
#. js-lingui-id: 7eVkEH
#: src/pages/onboarding/InviteTeam.tsx
msgid "Copy invitation link"
@ -2412,6 +2423,11 @@ msgstr "Bağlantıyı kopyala"
msgid "Copy link to view"
msgstr "Bağlantıyı kopyala"
#. js-lingui-id: VBIlvI
#: src/modules/ai/components/TerminalOutput.tsx
msgid "Copy output"
msgstr ""
#. js-lingui-id: HvB+os
#: src/pages/settings/SettingsTwoFactorAuthenticationMethod.tsx
msgid "Copy paste the code below"
@ -3452,6 +3468,11 @@ msgstr "Noktalar ve virgül - {dotsAndCommaExample}"
msgid "Downgrade"
msgstr "Düşürme"
#. js-lingui-id: qnTwse
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Download {filename}"
msgstr ""
#. js-lingui-id: WcWS//
#: src/modules/settings/security/components/SSO/SettingsSSOSAMLForm.tsx
msgid "Download file"
@ -4352,6 +4373,7 @@ msgstr "Ekstra Kredi Kullanıldı"
#. js-lingui-id: 7Bj3x9
#: src/pages/settings/emailing-domains/utils/getEmailingDomainStatusText.ts
#: src/modules/settings/admin-panel/health-status/components/SettingsAdminQueueJobsTable.tsx
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Failed"
msgstr "Başarısız"
@ -4714,6 +4736,11 @@ msgstr "Genel"
msgid "General - Settings"
msgstr "Genel - Ayarlar"
#. js-lingui-id: wu9eMO
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Generated Files"
msgstr ""
#. js-lingui-id: DDcvSo
#: src/pages/settings/profile/appearance/components/LocalePicker.tsx
msgid "German"
@ -6670,6 +6697,11 @@ msgstr "Model yok. Lütfen iş yeri ayarlarında AI modellerini yapılandırın.
msgid "No option found"
msgstr "Seçenek bulunamadı"
#. js-lingui-id: XRc1G9
#: src/modules/ai/components/TerminalOutput.tsx
msgid "No output"
msgstr ""
#. js-lingui-id: tTItk7
#: src/modules/workflow/workflow-steps/components/WorkflowRunStepOutputDetail.tsx
msgid "No output available"
@ -7140,6 +7172,12 @@ msgstr "Diğer çalışma alanları"
msgid "Our team can help you set up your workspace to match your specific needs and workflows."
msgstr "Ekibimiz, özel ihtiyaçlarınıza ve iş akışlarınıza uyan çalışma alanınızı kurmanız için size yardımcı olabilir."
#. js-lingui-id: gh06VD
#: src/modules/ai/components/TerminalOutput.tsx
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Output"
msgstr ""
#. js-lingui-id: xvfn7l
#: src/modules/workflow/workflow-steps/workflow-actions/ai-agent-action/components/WorkflowOutputSchemaBuilder.tsx
msgid "Output Field {fieldNumber}"
@ -7585,6 +7623,11 @@ msgstr "Genel alan adı başarıyla silindi"
msgid "Public Domains"
msgstr "Genel Alan Adları"
#. js-lingui-id: GUpZQM
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Python Code Execution"
msgstr ""
#. js-lingui-id: 2vudsu
#: src/modules/command-menu/pages/page-layout/utils/getDateGranularityLabel.ts
msgid "Quarter"
@ -8029,6 +8072,11 @@ msgstr "Bir iş akışı çalıştırın ve yürütmelerini görmek için buraya
msgid "Run Function"
msgstr "İşlevi Çalıştır"
#. js-lingui-id: LTC198
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Running..."
msgstr ""
#. js-lingui-id: nji0/X
#: src/pages/settings/profile/appearance/components/LocalePicker.tsx
msgid "Russian"
@ -10192,6 +10240,11 @@ msgstr "Bekleniyor"
msgid "Waiting Children"
msgstr "Bekleyen Çocuklar"
#. js-lingui-id: TdQd9Q
#: src/modules/ai/components/TerminalOutput.tsx
msgid "Waiting for output..."
msgstr ""
#. js-lingui-id: zFSQY3
#: src/modules/activities/timeline-activities/rows/main-object/components/EventRowMainObject.tsx
msgid "was created by"

View file

@ -2070,6 +2070,11 @@ msgstr "Закрити"
msgid "Close command menu"
msgstr "Закрити командне меню"
#. js-lingui-id: EWPtMO
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Code"
msgstr ""
#. js-lingui-id: /8PmQ9
#: src/modules/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowEditActionServerlessFunction.tsx
msgid "Code Editor"
@ -2147,6 +2152,7 @@ msgstr "завершено"
#. js-lingui-id: qqWcBV
#: src/modules/settings/admin-panel/health-status/components/SettingsAdminQueueJobsTable.tsx
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Completed"
msgstr "Завершено"
@ -2397,6 +2403,11 @@ msgstr "Скопійовано в буфер обміну"
msgid "Copy"
msgstr "Копіювати"
#. js-lingui-id: NmPNJJ
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Copy code"
msgstr ""
#. js-lingui-id: 7eVkEH
#: src/pages/onboarding/InviteTeam.tsx
msgid "Copy invitation link"
@ -2412,6 +2423,11 @@ msgstr "Копіювати посилання"
msgid "Copy link to view"
msgstr "Скопіювати посилання для перегляду"
#. js-lingui-id: VBIlvI
#: src/modules/ai/components/TerminalOutput.tsx
msgid "Copy output"
msgstr ""
#. js-lingui-id: HvB+os
#: src/pages/settings/SettingsTwoFactorAuthenticationMethod.tsx
msgid "Copy paste the code below"
@ -3452,6 +3468,11 @@ msgstr "Крапки і кома - {dotsAndCommaExample}"
msgid "Downgrade"
msgstr "Понижити"
#. js-lingui-id: qnTwse
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Download {filename}"
msgstr ""
#. js-lingui-id: WcWS//
#: src/modules/settings/security/components/SSO/SettingsSSOSAMLForm.tsx
msgid "Download file"
@ -4352,6 +4373,7 @@ msgstr "Додаткові використані кредити"
#. js-lingui-id: 7Bj3x9
#: src/pages/settings/emailing-domains/utils/getEmailingDomainStatusText.ts
#: src/modules/settings/admin-panel/health-status/components/SettingsAdminQueueJobsTable.tsx
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Failed"
msgstr "Не вдалося"
@ -4714,6 +4736,11 @@ msgstr "Загальні"
msgid "General - Settings"
msgstr "Загальні - Налаштування"
#. js-lingui-id: wu9eMO
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Generated Files"
msgstr ""
#. js-lingui-id: DDcvSo
#: src/pages/settings/profile/appearance/components/LocalePicker.tsx
msgid "German"
@ -6670,6 +6697,11 @@ msgstr "Немає доступних моделей. Увімкніть кон
msgid "No option found"
msgstr "Опцій не знайдено"
#. js-lingui-id: XRc1G9
#: src/modules/ai/components/TerminalOutput.tsx
msgid "No output"
msgstr ""
#. js-lingui-id: tTItk7
#: src/modules/workflow/workflow-steps/components/WorkflowRunStepOutputDetail.tsx
msgid "No output available"
@ -7140,6 +7172,12 @@ msgstr "Інші робочі простори"
msgid "Our team can help you set up your workspace to match your specific needs and workflows."
msgstr "Наша команда може допомогти налаштувати ваш робочий простір відповідно до ваших конкретних потреб і процесів."
#. js-lingui-id: gh06VD
#: src/modules/ai/components/TerminalOutput.tsx
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Output"
msgstr ""
#. js-lingui-id: xvfn7l
#: src/modules/workflow/workflow-steps/workflow-actions/ai-agent-action/components/WorkflowOutputSchemaBuilder.tsx
msgid "Output Field {fieldNumber}"
@ -7585,6 +7623,11 @@ msgstr "Публічний домен успішно видалено"
msgid "Public Domains"
msgstr "Публічні домени"
#. js-lingui-id: GUpZQM
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Python Code Execution"
msgstr ""
#. js-lingui-id: 2vudsu
#: src/modules/command-menu/pages/page-layout/utils/getDateGranularityLabel.ts
msgid "Quarter"
@ -8029,6 +8072,11 @@ msgstr "Запустіть workflow і поверніться сюди, щоб
msgid "Run Function"
msgstr "Запустити функцію"
#. js-lingui-id: LTC198
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Running..."
msgstr ""
#. js-lingui-id: nji0/X
#: src/pages/settings/profile/appearance/components/LocalePicker.tsx
msgid "Russian"
@ -10194,6 +10242,11 @@ msgstr "Очікування"
msgid "Waiting Children"
msgstr "Очікування дітей"
#. js-lingui-id: TdQd9Q
#: src/modules/ai/components/TerminalOutput.tsx
msgid "Waiting for output..."
msgstr ""
#. js-lingui-id: zFSQY3
#: src/modules/activities/timeline-activities/rows/main-object/components/EventRowMainObject.tsx
msgid "was created by"

View file

@ -2070,6 +2070,11 @@ msgstr "Đóng"
msgid "Close command menu"
msgstr "Đóng menu lệnh"
#. js-lingui-id: EWPtMO
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Code"
msgstr ""
#. js-lingui-id: /8PmQ9
#: src/modules/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowEditActionServerlessFunction.tsx
msgid "Code Editor"
@ -2147,6 +2152,7 @@ msgstr "hoàn thành"
#. js-lingui-id: qqWcBV
#: src/modules/settings/admin-panel/health-status/components/SettingsAdminQueueJobsTable.tsx
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Completed"
msgstr "Hoàn thành"
@ -2397,6 +2403,11 @@ msgstr "Đã sao chép vào bảng tạm"
msgid "Copy"
msgstr "Sao chép"
#. js-lingui-id: NmPNJJ
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Copy code"
msgstr ""
#. js-lingui-id: 7eVkEH
#: src/pages/onboarding/InviteTeam.tsx
msgid "Copy invitation link"
@ -2412,6 +2423,11 @@ msgstr "Sao chép liên kết"
msgid "Copy link to view"
msgstr "Sao chép liên kết để xem"
#. js-lingui-id: VBIlvI
#: src/modules/ai/components/TerminalOutput.tsx
msgid "Copy output"
msgstr ""
#. js-lingui-id: HvB+os
#: src/pages/settings/SettingsTwoFactorAuthenticationMethod.tsx
msgid "Copy paste the code below"
@ -3452,6 +3468,11 @@ msgstr "Dấu chấm và dấu phẩy - {dotsAndCommaExample}"
msgid "Downgrade"
msgstr "Hạ cấp"
#. js-lingui-id: qnTwse
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Download {filename}"
msgstr ""
#. js-lingui-id: WcWS//
#: src/modules/settings/security/components/SSO/SettingsSSOSAMLForm.tsx
msgid "Download file"
@ -4352,6 +4373,7 @@ msgstr "Số tín dụng bổ sung đã sử dụng"
#. js-lingui-id: 7Bj3x9
#: src/pages/settings/emailing-domains/utils/getEmailingDomainStatusText.ts
#: src/modules/settings/admin-panel/health-status/components/SettingsAdminQueueJobsTable.tsx
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Failed"
msgstr "Thất bại"
@ -4714,6 +4736,11 @@ msgstr "\\"
msgid "General - Settings"
msgstr "Cài đặt - Chung"
#. js-lingui-id: wu9eMO
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Generated Files"
msgstr ""
#. js-lingui-id: DDcvSo
#: src/pages/settings/profile/appearance/components/LocalePicker.tsx
msgid "German"
@ -6670,6 +6697,11 @@ msgstr "Không có mô hình nào có sẵn. Vui lòng cấu hình mô hình AI
msgid "No option found"
msgstr "Không có tuỳ chọn nào"
#. js-lingui-id: XRc1G9
#: src/modules/ai/components/TerminalOutput.tsx
msgid "No output"
msgstr ""
#. js-lingui-id: tTItk7
#: src/modules/workflow/workflow-steps/components/WorkflowRunStepOutputDetail.tsx
msgid "No output available"
@ -7140,6 +7172,12 @@ msgstr "Không gian làm việc khác"
msgid "Our team can help you set up your workspace to match your specific needs and workflows."
msgstr "Đội ngũ của chúng tôi có thể giúp bạn thiết lập không gian làm việc sao cho phù hợp với nhu cầu và quy trình làm việc cụ thể của bạn."
#. js-lingui-id: gh06VD
#: src/modules/ai/components/TerminalOutput.tsx
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Output"
msgstr ""
#. js-lingui-id: xvfn7l
#: src/modules/workflow/workflow-steps/workflow-actions/ai-agent-action/components/WorkflowOutputSchemaBuilder.tsx
msgid "Output Field {fieldNumber}"
@ -7585,6 +7623,11 @@ msgstr "Tên miền công cộng đã được xóa thành công"
msgid "Public Domains"
msgstr "Các tên miền công cộng"
#. js-lingui-id: GUpZQM
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Python Code Execution"
msgstr ""
#. js-lingui-id: 2vudsu
#: src/modules/command-menu/pages/page-layout/utils/getDateGranularityLabel.ts
msgid "Quarter"
@ -8029,6 +8072,11 @@ msgstr "Chạy một quy trình làm việc và trở lại đây để xem các
msgid "Run Function"
msgstr "Chạy chức năng"
#. js-lingui-id: LTC198
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Running..."
msgstr ""
#. js-lingui-id: nji0/X
#: src/pages/settings/profile/appearance/components/LocalePicker.tsx
msgid "Russian"
@ -10192,6 +10240,11 @@ msgstr "Đang chờ"
msgid "Waiting Children"
msgstr "Đang chờ trẻ em"
#. js-lingui-id: TdQd9Q
#: src/modules/ai/components/TerminalOutput.tsx
msgid "Waiting for output..."
msgstr ""
#. js-lingui-id: zFSQY3
#: src/modules/activities/timeline-activities/rows/main-object/components/EventRowMainObject.tsx
msgid "was created by"

View file

@ -2070,6 +2070,11 @@ msgstr "关闭"
msgid "Close command menu"
msgstr "关闭命令菜单"
#. js-lingui-id: EWPtMO
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Code"
msgstr ""
#. js-lingui-id: /8PmQ9
#: src/modules/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowEditActionServerlessFunction.tsx
msgid "Code Editor"
@ -2147,6 +2152,7 @@ msgstr "已完成"
#. js-lingui-id: qqWcBV
#: src/modules/settings/admin-panel/health-status/components/SettingsAdminQueueJobsTable.tsx
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Completed"
msgstr "已完成"
@ -2397,6 +2403,11 @@ msgstr "已复制到剪贴板"
msgid "Copy"
msgstr "复制"
#. js-lingui-id: NmPNJJ
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Copy code"
msgstr ""
#. js-lingui-id: 7eVkEH
#: src/pages/onboarding/InviteTeam.tsx
msgid "Copy invitation link"
@ -2412,6 +2423,11 @@ msgstr "复制链接"
msgid "Copy link to view"
msgstr "复制查看链接"
#. js-lingui-id: VBIlvI
#: src/modules/ai/components/TerminalOutput.tsx
msgid "Copy output"
msgstr ""
#. js-lingui-id: HvB+os
#: src/pages/settings/SettingsTwoFactorAuthenticationMethod.tsx
msgid "Copy paste the code below"
@ -3452,6 +3468,11 @@ msgstr "点和逗号 - {dotsAndCommaExample}"
msgid "Downgrade"
msgstr "降级"
#. js-lingui-id: qnTwse
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Download {filename}"
msgstr ""
#. js-lingui-id: WcWS//
#: src/modules/settings/security/components/SSO/SettingsSSOSAMLForm.tsx
msgid "Download file"
@ -4352,6 +4373,7 @@ msgstr "额外消费学分"
#. js-lingui-id: 7Bj3x9
#: src/pages/settings/emailing-domains/utils/getEmailingDomainStatusText.ts
#: src/modules/settings/admin-panel/health-status/components/SettingsAdminQueueJobsTable.tsx
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Failed"
msgstr "失败"
@ -4714,6 +4736,11 @@ msgstr "常规"
msgid "General - Settings"
msgstr "通用 - 设置"
#. js-lingui-id: wu9eMO
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Generated Files"
msgstr ""
#. js-lingui-id: DDcvSo
#: src/pages/settings/profile/appearance/components/LocalePicker.tsx
msgid "German"
@ -6670,6 +6697,11 @@ msgstr "没有可用的模型。请在工作区设置中配置 AI 模型。"
msgid "No option found"
msgstr "未找到选项"
#. js-lingui-id: XRc1G9
#: src/modules/ai/components/TerminalOutput.tsx
msgid "No output"
msgstr ""
#. js-lingui-id: tTItk7
#: src/modules/workflow/workflow-steps/components/WorkflowRunStepOutputDetail.tsx
msgid "No output available"
@ -7140,6 +7172,12 @@ msgstr "其他工作区"
msgid "Our team can help you set up your workspace to match your specific needs and workflows."
msgstr "我们的团队可以帮助您设置工作空间,以符合您的特定需求和工作流程。"
#. js-lingui-id: gh06VD
#: src/modules/ai/components/TerminalOutput.tsx
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Output"
msgstr ""
#. js-lingui-id: xvfn7l
#: src/modules/workflow/workflow-steps/workflow-actions/ai-agent-action/components/WorkflowOutputSchemaBuilder.tsx
msgid "Output Field {fieldNumber}"
@ -7585,6 +7623,11 @@ msgstr "公共域删除成功"
msgid "Public Domains"
msgstr "公共域"
#. js-lingui-id: GUpZQM
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Python Code Execution"
msgstr ""
#. js-lingui-id: 2vudsu
#: src/modules/command-menu/pages/page-layout/utils/getDateGranularityLabel.ts
msgid "Quarter"
@ -8029,6 +8072,11 @@ msgstr "运行工作流并返回此处查看其执行情况"
msgid "Run Function"
msgstr "运行函数"
#. js-lingui-id: LTC198
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Running..."
msgstr ""
#. js-lingui-id: nji0/X
#: src/pages/settings/profile/appearance/components/LocalePicker.tsx
msgid "Russian"
@ -10192,6 +10240,11 @@ msgstr "等待中"
msgid "Waiting Children"
msgstr "等待子任务"
#. js-lingui-id: TdQd9Q
#: src/modules/ai/components/TerminalOutput.tsx
msgid "Waiting for output..."
msgstr ""
#. js-lingui-id: zFSQY3
#: src/modules/activities/timeline-activities/rows/main-object/components/EventRowMainObject.tsx
msgid "was created by"

View file

@ -2070,6 +2070,11 @@ msgstr "關閉"
msgid "Close command menu"
msgstr "關閉指令選單"
#. js-lingui-id: EWPtMO
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Code"
msgstr ""
#. js-lingui-id: /8PmQ9
#: src/modules/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowEditActionServerlessFunction.tsx
msgid "Code Editor"
@ -2147,6 +2152,7 @@ msgstr "完成"
#. js-lingui-id: qqWcBV
#: src/modules/settings/admin-panel/health-status/components/SettingsAdminQueueJobsTable.tsx
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Completed"
msgstr "完成"
@ -2397,6 +2403,11 @@ msgstr "已複製到剪貼板"
msgid "Copy"
msgstr "複製"
#. js-lingui-id: NmPNJJ
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Copy code"
msgstr ""
#. js-lingui-id: 7eVkEH
#: src/pages/onboarding/InviteTeam.tsx
msgid "Copy invitation link"
@ -2412,6 +2423,11 @@ msgstr "複製連結"
msgid "Copy link to view"
msgstr "複製連結以查看"
#. js-lingui-id: VBIlvI
#: src/modules/ai/components/TerminalOutput.tsx
msgid "Copy output"
msgstr ""
#. js-lingui-id: HvB+os
#: src/pages/settings/SettingsTwoFactorAuthenticationMethod.tsx
msgid "Copy paste the code below"
@ -3452,6 +3468,11 @@ msgstr "點和逗號 - {dotsAndCommaExample}"
msgid "Downgrade"
msgstr "降級"
#. js-lingui-id: qnTwse
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Download {filename}"
msgstr ""
#. js-lingui-id: WcWS//
#: src/modules/settings/security/components/SSO/SettingsSSOSAMLForm.tsx
msgid "Download file"
@ -4352,6 +4373,7 @@ msgstr "額外的信用使用情況"
#. js-lingui-id: 7Bj3x9
#: src/pages/settings/emailing-domains/utils/getEmailingDomainStatusText.ts
#: src/modules/settings/admin-panel/health-status/components/SettingsAdminQueueJobsTable.tsx
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Failed"
msgstr "失敗"
@ -4714,6 +4736,11 @@ msgstr "常規"
msgid "General - Settings"
msgstr "一般 - 設定"
#. js-lingui-id: wu9eMO
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Generated Files"
msgstr ""
#. js-lingui-id: DDcvSo
#: src/pages/settings/profile/appearance/components/LocalePicker.tsx
msgid "German"
@ -6670,6 +6697,11 @@ msgstr "無模型可用。請在工作區設置中配置AI模型。"
msgid "No option found"
msgstr "未找到選項"
#. js-lingui-id: XRc1G9
#: src/modules/ai/components/TerminalOutput.tsx
msgid "No output"
msgstr ""
#. js-lingui-id: tTItk7
#: src/modules/workflow/workflow-steps/components/WorkflowRunStepOutputDetail.tsx
msgid "No output available"
@ -7140,6 +7172,12 @@ msgstr "其他工作區"
msgid "Our team can help you set up your workspace to match your specific needs and workflows."
msgstr "我們的團隊可以幫助您設置工作區,以滿足您的具體需求和工作流程。"
#. js-lingui-id: gh06VD
#: src/modules/ai/components/TerminalOutput.tsx
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Output"
msgstr ""
#. js-lingui-id: xvfn7l
#: src/modules/workflow/workflow-steps/workflow-actions/ai-agent-action/components/WorkflowOutputSchemaBuilder.tsx
msgid "Output Field {fieldNumber}"
@ -7585,6 +7623,11 @@ msgstr "公有域名成功刪除"
msgid "Public Domains"
msgstr "公有域名"
#. js-lingui-id: GUpZQM
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Python Code Execution"
msgstr ""
#. js-lingui-id: 2vudsu
#: src/modules/command-menu/pages/page-layout/utils/getDateGranularityLabel.ts
msgid "Quarter"
@ -8029,6 +8072,11 @@ msgstr "運行工作流程並返回此處查看其執行情況"
msgid "Run Function"
msgstr "執行功能"
#. js-lingui-id: LTC198
#: src/modules/ai/components/CodeExecutionDisplay.tsx
msgid "Running..."
msgstr ""
#. js-lingui-id: nji0/X
#: src/pages/settings/profile/appearance/components/LocalePicker.tsx
msgid "Russian"
@ -10192,6 +10240,11 @@ msgstr "等待中"
msgid "Waiting Children"
msgstr "等待的子項"
#. js-lingui-id: TdQd9Q
#: src/modules/ai/components/TerminalOutput.tsx
msgid "Waiting for output..."
msgstr ""
#. js-lingui-id: zFSQY3
#: src/modules/activities/timeline-activities/rows/main-object/components/EventRowMainObject.tsx
msgid "was created by"

View file

@ -1,3 +1,4 @@
import { CodeExecutionDisplay } from '@/ai/components/CodeExecutionDisplay';
import { ReasoningSummaryDisplay } from '@/ai/components/ReasoningSummaryDisplay';
import { RoutingStatusDisplay } from '@/ai/components/RoutingStatusDisplay';
import { IconDotsVertical } from 'twenty-ui/display';
@ -65,6 +66,15 @@ export const AIChatAssistantMessageRenderer = ({
isLastMessageStreaming: boolean;
hasError?: boolean;
}) => {
// Filter out data-code-execution parts when tool-code_interpreter exists
// (the tool part contains the final result, data-code-execution is for streaming updates)
const hasCodeInterpreterTool = messageParts.some(
(part) => part.type === 'tool-code_interpreter',
);
const filteredParts = hasCodeInterpreterTool
? messageParts.filter((part) => part.type !== 'data-code-execution')
: messageParts;
const renderMessagePart = (part: ExtendedUIMessagePart, index: number) => {
switch (part.type) {
case 'reasoning':
@ -79,6 +89,20 @@ export const AIChatAssistantMessageRenderer = ({
return <LazyMarkdownRenderer key={index} text={part.text} />;
case 'data-routing-status':
return <RoutingStatusDisplay data={part.data} key={index} />;
case 'data-code-execution':
return (
<CodeExecutionDisplay
key={index}
code={part.data.code}
stdout={part.data.stdout}
stderr={part.data.stderr}
exitCode={part.data.exitCode}
files={part.data.files}
isRunning={
part.data.state === 'running' || part.data.state === 'pending'
}
/>
);
default:
{
if (isToolUIPart(part)) {
@ -89,14 +113,14 @@ export const AIChatAssistantMessageRenderer = ({
}
};
if (!messageParts.length && !hasError) {
if (!filteredParts.length && !hasError) {
return <InitialLoadingIndicator />;
}
return (
<div>
<StyledMessagePartsContainer>
{messageParts.map(renderMessagePart)}
{filteredParts.map(renderMessagePart)}
</StyledMessagePartsContainer>
{isLastMessageStreaming && !hasError && <StyledStreamingIndicator />}
</div>

View file

@ -0,0 +1,376 @@
import { TerminalOutput } from '@/ai/components/TerminalOutput';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { useLingui } from '@lingui/react/macro';
import { useState } from 'react';
import {
IconChevronDown,
IconChevronUp,
IconCode,
IconCopy,
IconDownload,
IconFile,
IconPlayerPlay,
IconSquareRoundedCheck,
IconSquareRoundedX,
} from 'twenty-ui/display';
import { CodeEditor, LightIconButton } from 'twenty-ui/input';
import { AnimatedExpandableContainer } from 'twenty-ui/layout';
import { useCopyToClipboard } from '~/hooks/useCopyToClipboard';
const StyledContainer = styled.div`
border: 1px solid ${({ theme }) => theme.border.color.medium};
border-radius: ${({ theme }) => theme.border.radius.md};
display: flex;
flex-direction: column;
margin: ${({ theme }) => theme.spacing(2)} 0;
overflow: hidden;
`;
const StyledHeader = styled.div<{ status: 'success' | 'error' | 'running' }>`
align-items: center;
background: ${({ theme }) => theme.background.secondary};
border-bottom: 1px solid ${({ theme }) => theme.border.color.light};
display: flex;
gap: ${({ theme }) => theme.spacing(2)};
justify-content: space-between;
padding: ${({ theme }) => theme.spacing(2)} ${({ theme }) => theme.spacing(3)};
`;
const StyledHeaderLeft = styled.div`
align-items: center;
display: flex;
gap: ${({ theme }) => theme.spacing(2)};
`;
const StyledHeaderRight = styled.div`
align-items: center;
display: flex;
gap: ${({ theme }) => theme.spacing(1)};
`;
const StyledStatusBadge = styled.div<{
status: 'success' | 'error' | 'running';
}>`
align-items: center;
background: ${({ status, theme }) =>
status === 'success'
? theme.background.transparent.success
: status === 'error'
? theme.background.transparent.danger
: theme.background.transparent.medium};
border-radius: ${({ theme }) => theme.border.radius.pill};
color: ${({ status, theme }) =>
status === 'success'
? theme.color.turquoise
: status === 'error'
? theme.color.red
: theme.font.color.secondary};
display: flex;
font-size: ${({ theme }) => theme.font.size.xs};
font-weight: ${({ theme }) => theme.font.weight.medium};
gap: ${({ theme }) => theme.spacing(1)};
padding: ${({ theme }) => theme.spacing(0.5)}
${({ theme }) => theme.spacing(2)};
`;
const StyledTitle = styled.span`
color: ${({ theme }) => theme.font.color.primary};
font-size: ${({ theme }) => theme.font.size.md};
font-weight: ${({ theme }) => theme.font.weight.medium};
`;
const StyledSection = styled.div`
border-top: 1px solid ${({ theme }) => theme.border.color.light};
`;
const StyledSectionHeader = styled.div`
align-items: center;
background: ${({ theme }) => theme.background.secondary};
cursor: pointer;
display: flex;
gap: ${({ theme }) => theme.spacing(2)};
justify-content: space-between;
padding: ${({ theme }) => theme.spacing(2)} ${({ theme }) => theme.spacing(3)};
transition: background ${({ theme }) => theme.animation.duration.fast}s;
&:hover {
background: ${({ theme }) => theme.background.tertiary};
}
`;
const StyledSectionHeaderLeft = styled.div`
align-items: center;
color: ${({ theme }) => theme.font.color.secondary};
display: flex;
font-size: ${({ theme }) => theme.font.size.sm};
font-weight: ${({ theme }) => theme.font.weight.medium};
gap: ${({ theme }) => theme.spacing(1)};
`;
const StyledCodeEditorContainer = styled.div`
max-height: 300px;
overflow: hidden;
`;
const StyledFilesGrid = styled.div`
display: grid;
gap: ${({ theme }) => theme.spacing(2)};
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
padding: ${({ theme }) => theme.spacing(3)};
`;
const StyledFileCard = styled.div`
border: 1px solid ${({ theme }) => theme.border.color.light};
border-radius: ${({ theme }) => theme.border.radius.sm};
display: flex;
flex-direction: column;
overflow: hidden;
`;
const StyledFilePreview = styled.div`
align-items: center;
aspect-ratio: 16 / 9;
background: ${({ theme }) => theme.background.tertiary};
display: flex;
justify-content: center;
overflow: hidden;
`;
const StyledPreviewImage = styled.img`
height: 100%;
object-fit: contain;
width: 100%;
`;
const StyledFileInfo = styled.div`
align-items: center;
background: ${({ theme }) => theme.background.secondary};
display: flex;
gap: ${({ theme }) => theme.spacing(2)};
justify-content: space-between;
padding: ${({ theme }) => theme.spacing(1.5)}
${({ theme }) => theme.spacing(2)};
`;
const StyledFileName = styled.span`
color: ${({ theme }) => theme.font.color.primary};
font-size: ${({ theme }) => theme.font.size.sm};
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
`;
const StyledDownloadLink = styled.a`
align-items: center;
color: ${({ theme }) => theme.font.color.secondary};
display: flex;
&:hover {
color: ${({ theme }) => theme.font.color.primary};
}
`;
type CodeExecutionDisplayProps = {
code: string;
stdout: string;
stderr: string;
exitCode?: number;
files?: Array<{
filename: string;
url: string;
mimeType?: string;
}>;
isRunning?: boolean;
};
const isPreviewableMimeType = (mimeType?: string): boolean => {
if (!mimeType) {
return false;
}
return ['image/png', 'image/jpeg', 'image/gif', 'image/webp'].includes(
mimeType,
);
};
export const CodeExecutionDisplay = ({
code,
stdout,
stderr,
exitCode,
files = [],
isRunning = false,
}: CodeExecutionDisplayProps) => {
const { t } = useLingui();
const theme = useTheme();
const { copyToClipboard } = useCopyToClipboard();
const [isCodeExpanded, setIsCodeExpanded] = useState(false);
const [isOutputExpanded, setIsOutputExpanded] = useState(true);
const [isFilesExpanded, setIsFilesExpanded] = useState(true);
const status: 'success' | 'error' | 'running' = isRunning
? 'running'
: exitCode === 0
? 'success'
: 'error';
const StatusIcon =
status === 'success'
? IconSquareRoundedCheck
: status === 'error'
? IconSquareRoundedX
: IconPlayerPlay;
const statusText = isRunning
? t`Running...`
: exitCode === 0
? t`Completed`
: t`Failed`;
const hasOutput = stdout || stderr;
const hasFiles = files.length > 0;
return (
<StyledContainer>
<StyledHeader status={status}>
<StyledHeaderLeft>
<IconCode size={theme.icon.size.md} />
<StyledTitle>{t`Python Code Execution`}</StyledTitle>
</StyledHeaderLeft>
<StyledHeaderRight>
<StyledStatusBadge status={status}>
<StatusIcon size={theme.icon.size.sm} />
{statusText}
</StyledStatusBadge>
</StyledHeaderRight>
</StyledHeader>
<StyledSection>
<StyledSectionHeader onClick={() => setIsCodeExpanded(!isCodeExpanded)}>
<StyledSectionHeaderLeft>
<IconCode size={theme.icon.size.sm} />
{t`Code`}
</StyledSectionHeaderLeft>
<StyledHeaderRight>
<LightIconButton
Icon={IconCopy}
onClick={(e) => {
e.stopPropagation();
copyToClipboard(code);
}}
title={t`Copy code`}
size="small"
accent="tertiary"
/>
{isCodeExpanded ? (
<IconChevronUp size={theme.icon.size.sm} />
) : (
<IconChevronDown size={theme.icon.size.sm} />
)}
</StyledHeaderRight>
</StyledSectionHeader>
<AnimatedExpandableContainer
isExpanded={isCodeExpanded}
mode="fit-content"
>
<StyledCodeEditorContainer>
<CodeEditor
value={code}
language="python"
height="300px"
options={{
readOnly: true,
domReadOnly: true,
minimap: { enabled: false },
}}
/>
</StyledCodeEditorContainer>
</AnimatedExpandableContainer>
</StyledSection>
{(hasOutput || isRunning) && (
<StyledSection>
<StyledSectionHeader
onClick={() => setIsOutputExpanded(!isOutputExpanded)}
>
<StyledSectionHeaderLeft>{t`Output`}</StyledSectionHeaderLeft>
{isOutputExpanded ? (
<IconChevronUp size={theme.icon.size.sm} />
) : (
<IconChevronDown size={theme.icon.size.sm} />
)}
</StyledSectionHeader>
<AnimatedExpandableContainer
isExpanded={isOutputExpanded}
mode="fit-content"
>
<TerminalOutput
stdout={stdout}
stderr={stderr}
isRunning={isRunning}
/>
</AnimatedExpandableContainer>
</StyledSection>
)}
{hasFiles && (
<StyledSection>
<StyledSectionHeader
onClick={() => setIsFilesExpanded(!isFilesExpanded)}
>
<StyledSectionHeaderLeft>
<IconFile size={theme.icon.size.sm} />
{t`Generated Files`} ({files.length})
</StyledSectionHeaderLeft>
{isFilesExpanded ? (
<IconChevronUp size={theme.icon.size.sm} />
) : (
<IconChevronDown size={theme.icon.size.sm} />
)}
</StyledSectionHeader>
<AnimatedExpandableContainer
isExpanded={isFilesExpanded}
mode="fit-content"
>
<StyledFilesGrid>
{files.map((file) => {
const filename = file.filename;
return (
<StyledFileCard key={file.url}>
<StyledFilePreview>
{isPreviewableMimeType(file.mimeType) ? (
<StyledPreviewImage
src={file.url}
alt={filename}
loading="lazy"
/>
) : (
<IconFile size={48} color={theme.font.color.tertiary} />
)}
</StyledFilePreview>
<StyledFileInfo>
<StyledFileName title={filename}>
{filename}
</StyledFileName>
<StyledDownloadLink
href={file.url}
download={filename}
title={t`Download ${filename}`}
target="_blank"
rel="noopener noreferrer"
>
<IconDownload size={theme.icon.size.sm} />
</StyledDownloadLink>
</StyledFileInfo>
</StyledFileCard>
);
})}
</StyledFilesGrid>
</AnimatedExpandableContainer>
</StyledSection>
)}
</StyledContainer>
);
};

View file

@ -0,0 +1,174 @@
import styled from '@emotion/styled';
import { useLingui } from '@lingui/react/macro';
import { useState } from 'react';
import { IconCopy, IconTerminal } from 'twenty-ui/display';
import { LightIconButton } from 'twenty-ui/input';
import { useCopyToClipboard } from '~/hooks/useCopyToClipboard';
const StyledContainer = styled.div`
display: flex;
flex-direction: column;
border-radius: ${({ theme }) => theme.border.radius.sm};
overflow: hidden;
`;
const StyledHeader = styled.div`
align-items: center;
background: ${({ theme }) => theme.background.tertiary};
border-bottom: 1px solid ${({ theme }) => theme.border.color.medium};
display: flex;
gap: ${({ theme }) => theme.spacing(2)};
justify-content: space-between;
padding: ${({ theme }) => theme.spacing(1)} ${({ theme }) => theme.spacing(2)};
`;
const StyledHeaderLeft = styled.div`
align-items: center;
color: ${({ theme }) => theme.font.color.secondary};
display: flex;
font-size: ${({ theme }) => theme.font.size.sm};
gap: ${({ theme }) => theme.spacing(1)};
`;
const StyledTabContainer = styled.div`
display: flex;
gap: ${({ theme }) => theme.spacing(1)};
`;
const StyledTab = styled.button<{ isActive: boolean; hasError?: boolean }>`
background: ${({ isActive, theme }) =>
isActive ? theme.background.secondary : 'transparent'};
border: none;
border-radius: ${({ theme }) => theme.border.radius.xs};
color: ${({ isActive, hasError, theme }) =>
hasError
? theme.color.red
: isActive
? theme.font.color.primary
: theme.font.color.tertiary};
cursor: pointer;
font-size: ${({ theme }) => theme.font.size.xs};
font-weight: ${({ isActive, theme }) =>
isActive ? theme.font.weight.medium : theme.font.weight.regular};
padding: ${({ theme }) => theme.spacing(0.5)}
${({ theme }) => theme.spacing(1)};
&:hover {
background: ${({ theme }) => theme.background.secondary};
color: ${({ hasError, theme }) =>
hasError ? theme.color.red : theme.font.color.primary};
}
`;
const StyledOutputArea = styled.div<{ isError?: boolean }>`
background: ${({ theme }) => theme.background.tertiary};
color: ${({ isError, theme }) =>
isError ? theme.color.red : theme.font.color.primary};
font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Roboto Mono', monospace;
font-size: ${({ theme }) => theme.font.size.sm};
line-height: 1.5;
max-height: 300px;
min-height: 100px;
overflow-y: auto;
padding: ${({ theme }) => theme.spacing(2)};
white-space: pre-wrap;
word-break: break-word;
`;
const StyledEmptyMessage = styled.div`
color: ${({ theme }) => theme.font.color.tertiary};
font-style: italic;
`;
const StyledCursor = styled.span`
animation: blink 1s step-end infinite;
background: ${({ theme }) => theme.font.color.primary};
display: inline-block;
height: 1em;
margin-left: 2px;
vertical-align: text-bottom;
width: 8px;
@keyframes blink {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0;
}
}
`;
type TabType = 'stdout' | 'stderr';
type TerminalOutputProps = {
stdout: string;
stderr: string;
isRunning?: boolean;
};
export const TerminalOutput = ({
stdout,
stderr,
isRunning = false,
}: TerminalOutputProps) => {
const { t } = useLingui();
const { copyToClipboard } = useCopyToClipboard();
const hasStderr = stderr.length > 0;
const hasStdout = stdout.length > 0;
const defaultTab: TabType = hasStderr && !hasStdout ? 'stderr' : 'stdout';
const [userSelectedTab, setUserSelectedTab] = useState<TabType | null>(null);
const activeTab = userSelectedTab ?? defaultTab;
const currentOutput = activeTab === 'stdout' ? stdout : stderr;
return (
<StyledContainer>
<StyledHeader>
<StyledHeaderLeft>
<IconTerminal size={14} />
{t`Output`}
</StyledHeaderLeft>
<StyledTabContainer>
<StyledTab
isActive={activeTab === 'stdout'}
onClick={() => setUserSelectedTab('stdout')}
>
stdout
</StyledTab>
{hasStderr && (
<StyledTab
isActive={activeTab === 'stderr'}
hasError
onClick={() => setUserSelectedTab('stderr')}
>
stderr
</StyledTab>
)}
<LightIconButton
Icon={IconCopy}
onClick={() => copyToClipboard(currentOutput)}
title={t`Copy output`}
size="small"
accent="tertiary"
/>
</StyledTabContainer>
</StyledHeader>
<StyledOutputArea isError={activeTab === 'stderr'}>
{currentOutput ? (
<>
{currentOutput}
{isRunning && <StyledCursor />}
</>
) : (
<StyledEmptyMessage>
{isRunning ? t`Waiting for output...` : t`No output`}
</StyledEmptyMessage>
)}
</StyledOutputArea>
</StyledContainer>
);
};

View file

@ -6,6 +6,7 @@ import { IconChevronDown, IconChevronUp } from 'twenty-ui/display';
import { JsonTree } from 'twenty-ui/json-visualizer';
import { AnimatedExpandableContainer } from 'twenty-ui/layout';
import { CodeExecutionDisplay } from '@/ai/components/CodeExecutionDisplay';
import { ShimmeringText } from '@/ai/components/ShimmeringText';
import { getToolIcon } from '@/ai/utils/getToolIcon';
import { getToolDisplayMessage } from '@/ai/utils/getWebSearchToolDisplayMessage';
@ -141,6 +142,32 @@ export const ToolStepRenderer = ({ toolPart }: { toolPart: ToolUIPart }) => {
const hasError = isDefined(errorText);
const isExpandable = isDefined(output) || hasError;
// Special handling for code_interpreter tool
if (toolName === 'code_interpreter') {
const codeInput = toolInput as { code?: string } | undefined;
const codeOutput = output as {
result?: {
stdout?: string;
stderr?: string;
exitCode?: number;
files?: Array<{ filename: string; url: string; mimeType?: string }>;
};
} | null;
const isRunning = !output && !hasError;
return (
<CodeExecutionDisplay
code={codeInput?.code ?? ''}
stdout={codeOutput?.result?.stdout ?? ''}
stderr={codeOutput?.result?.stderr || errorText || ''}
exitCode={codeOutput?.result?.exitCode}
files={codeOutput?.result?.files}
isRunning={isRunning}
/>
);
}
if (!output && !hasError) {
return (
<StyledContainer>

View file

@ -0,0 +1,239 @@
import styled from '@emotion/styled';
import { type Meta, type StoryObj } from '@storybook/react';
import { type ExtendedUIMessage } from 'twenty-shared/ai';
import { ComponentDecorator } from 'twenty-ui/testing';
import { AIChatMessage } from '@/ai/components/AIChatMessage';
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
import { RootDecorator } from '~/testing/decorators/RootDecorator';
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
const StyledConversationContainer = styled.div`
display: flex;
flex-direction: column;
gap: 24px;
max-width: 700px;
padding: 24px;
`;
// Mock messages for the conversation showcase
const mockUserMessage: ExtendedUIMessage = {
id: 'msg-user-1',
role: 'user',
parts: [
{
type: 'text',
text: 'Can you analyze my sales data and create a chart showing the monthly trends?',
},
],
metadata: {
createdAt: new Date(Date.now() - 120000).toISOString(),
},
};
const mockAssistantWithCodeExecution: ExtendedUIMessage = {
id: 'msg-assistant-1',
role: 'assistant',
parts: [
{
type: 'data-code-execution',
data: {
executionId: 'exec-1',
state: 'completed',
code: `import pandas as pd
import matplotlib.pyplot as plt
# Load and process sales data
data = {
'month': ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'],
'sales': [12500, 15200, 14800, 18900, 21000, 19500]
}
df = pd.DataFrame(data)
# Create the chart
plt.figure(figsize=(10, 6))
plt.bar(df['month'], df['sales'], color='steelblue')
plt.title('Monthly Sales Trends')
plt.xlabel('Month')
plt.ylabel('Sales ($)')
plt.savefig('sales_chart.png', dpi=150)
print(f"Total sales: $" + str(df['sales'].sum()))
print("Chart saved successfully!")`,
language: 'python',
stdout: 'Total sales: $101,900\nChart saved successfully!',
stderr: '',
exitCode: 0,
executionTimeMs: 2340,
files: [
{
filename: 'sales_chart.png',
url: 'https://picsum.photos/800/480',
mimeType: 'image/png',
},
],
},
},
{
type: 'text',
text: "I've analyzed your sales data and created a chart showing the monthly trends. Here are the key insights:\n\n- **Total sales**: $101,900 over 6 months\n- **Peak month**: May with $21,000 in sales\n- **Growth trend**: Overall positive trajectory with 68% growth from January to May\n\nThe chart shows a clear upward trend with a slight dip in March. Would you like me to perform any additional analysis?",
},
],
metadata: {
createdAt: new Date(Date.now() - 60000).toISOString(),
usage: {
inputTokens: 1250,
outputTokens: 890,
inputCredits: 12,
outputCredits: 8,
},
},
};
const mockSimpleTextResponse: ExtendedUIMessage = {
id: 'msg-assistant-text',
role: 'assistant',
parts: [
{
type: 'text',
text: "Hello! I'm your AI assistant. I can help you with:\n\n- **Data analysis** - Analyze your CRM data and generate insights\n- **Code execution** - Run Python code for complex calculations\n- **Record management** - Create, update, or find records\n\nHow can I assist you today?",
},
],
metadata: {
createdAt: new Date().toISOString(),
},
};
const mockStreamingMessage: ExtendedUIMessage = {
id: 'msg-streaming',
role: 'assistant',
parts: [
{
type: 'text',
text: 'Let me look into that for you',
},
],
metadata: {
createdAt: new Date().toISOString(),
},
};
const mockCodeExecutionRunning: ExtendedUIMessage = {
id: 'msg-code-running',
role: 'assistant',
parts: [
{
type: 'data-code-execution',
data: {
executionId: 'exec-running',
state: 'running',
code: `import time
print("Processing data...")
time.sleep(5)
print("Done!")`,
language: 'python',
stdout: 'Processing data...',
stderr: '',
files: [],
},
},
],
metadata: {
createdAt: new Date().toISOString(),
},
};
const mockCodeExecutionError: ExtendedUIMessage = {
id: 'msg-code-error',
role: 'assistant',
parts: [
{
type: 'data-code-execution',
data: {
executionId: 'exec-error',
state: 'error',
code: `import pandas as pd
df = pd.read_csv('missing_file.csv')
print(df.head())`,
language: 'python',
stdout: '',
stderr:
"FileNotFoundError: [Errno 2] No such file or directory: 'missing_file.csv'",
exitCode: 1,
files: [],
error: 'File not found',
},
},
{
type: 'text',
text: "I encountered an error while trying to read the file. It looks like the file `missing_file.csv` doesn't exist. Could you please check the file path or upload the file you'd like me to analyze?",
},
],
metadata: {
createdAt: new Date().toISOString(),
},
};
const meta: Meta<typeof AIChatMessage> = {
title: 'Modules/AI/AIChatMessage',
component: AIChatMessage,
decorators: [
ComponentDecorator,
RootDecorator,
I18nFrontDecorator,
SnackBarDecorator,
],
parameters: {
container: { width: 700 },
},
};
export default meta;
type Story = StoryObj<typeof AIChatMessage>;
// Conversation showcase - demonstrates a full AI chat flow
export const ConversationWithCodeExecution: Story = {
render: () => (
<StyledConversationContainer>
<AIChatMessage message={mockUserMessage} isLastMessageStreaming={false} />
<AIChatMessage
message={mockAssistantWithCodeExecution}
isLastMessageStreaming={false}
/>
</StyledConversationContainer>
),
};
export const UserMessage: Story = {
args: {
message: mockUserMessage,
isLastMessageStreaming: false,
},
};
export const AssistantTextResponse: Story = {
args: {
message: mockSimpleTextResponse,
isLastMessageStreaming: false,
},
};
export const AssistantStreaming: Story = {
args: {
message: mockStreamingMessage,
isLastMessageStreaming: true,
},
};
export const CodeExecutionRunning: Story = {
args: {
message: mockCodeExecutionRunning,
isLastMessageStreaming: false,
},
};
export const CodeExecutionWithError: Story = {
args: {
message: mockCodeExecutionError,
isLastMessageStreaming: false,
},
};

View file

@ -0,0 +1,209 @@
import { type Meta, type StoryObj } from '@storybook/react';
import { expect, userEvent, within } from '@storybook/test';
import { ComponentDecorator } from 'twenty-ui/testing';
import { CodeExecutionDisplay } from '@/ai/components/CodeExecutionDisplay';
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
const samplePythonCode = `import pandas as pd
import matplotlib.pyplot as plt
# Create sample data
data = {
'month': ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'],
'revenue': [12500, 15200, 14800, 18900, 21000, 19500]
}
df = pd.DataFrame(data)
# Calculate statistics
total = df['revenue'].sum()
average = df['revenue'].mean()
print(f"Total Revenue: $" + f"{total:,.2f}")
print(f"Average Monthly: $" + f"{average:,.2f}")
# Generate chart
plt.figure(figsize=(10, 6))
plt.bar(df['month'], df['revenue'], color='steelblue')
plt.title('Monthly Revenue')
plt.savefig('revenue_chart.png')`;
const meta: Meta<typeof CodeExecutionDisplay> = {
title: 'Modules/AI/CodeExecutionDisplay',
component: CodeExecutionDisplay,
decorators: [I18nFrontDecorator, SnackBarDecorator, ComponentDecorator],
parameters: {
container: { width: 600 },
},
args: {
code: samplePythonCode,
stdout: '',
stderr: '',
isRunning: false,
},
};
export default meta;
type Story = StoryObj<typeof CodeExecutionDisplay>;
export const Running: Story = {
args: {
code: `print("Processing data...")
import time
time.sleep(5)
print("Complete!")`,
stdout: 'Processing data...',
stderr: '',
isRunning: true,
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
expect(await canvas.findByText('Running...')).toBeVisible();
},
};
export const Success: Story = {
args: {
code: samplePythonCode,
stdout: 'Total Revenue: $101,900.00\nAverage Monthly: $16,983.33',
stderr: '',
exitCode: 0,
isRunning: false,
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
expect(await canvas.findByText('Completed')).toBeVisible();
// Output content is inside a scrollable container
expect(await canvas.findByText(/Total Revenue/)).toBeInTheDocument();
},
};
export const Error: Story = {
args: {
code: `import pandas as pd
df = pd.read_csv('missing_file.csv')
print(df.head())`,
stdout: '',
stderr:
'Traceback (most recent call last):\n File "<stdin>", line 2, in <module>\nFileNotFoundError: [Errno 2] No such file or directory: \'missing_file.csv\'',
exitCode: 1,
isRunning: false,
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
expect(await canvas.findByText('Failed')).toBeVisible();
},
};
export const WithImageFiles: Story = {
args: {
code: samplePythonCode,
stdout: 'Chart generated successfully!',
stderr: '',
exitCode: 0,
isRunning: false,
files: [
{
filename: 'revenue_chart.png',
url: 'https://picsum.photos/800/480',
mimeType: 'image/png',
},
{
filename: 'pie_chart.png',
url: 'https://picsum.photos/600/400',
mimeType: 'image/png',
},
],
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
// Text includes file count: "Generated Files (2)"
expect(await canvas.findByText(/Generated Files/)).toBeInTheDocument();
// Filenames may be truncated, check by title attribute
expect(await canvas.findByTitle('revenue_chart.png')).toBeInTheDocument();
expect(await canvas.findByTitle('pie_chart.png')).toBeInTheDocument();
},
};
export const WithDownloadableFiles: Story = {
args: {
code: `import pandas as pd
df = pd.DataFrame({
'name': ['Alice', 'Bob', 'Charlie'],
'sales': [1200, 1500, 980]
})
df.to_csv('report.csv', index=False)
df.to_json('data.json')
print("Files exported successfully!")`,
stdout: 'Files exported successfully!',
stderr: '',
exitCode: 0,
isRunning: false,
files: [
{
filename: 'report.csv',
url: 'data:text/csv,name%2Csales%0AAlice%2C1200%0ABob%2C1500',
mimeType: 'text/csv',
},
{
filename: 'data.json',
url: 'data:application/json,%7B%22name%22%3A%5B%22Alice%22%5D%7D',
mimeType: 'application/json',
},
],
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
// Filenames may be truncated, check by title attribute
expect(await canvas.findByTitle('report.csv')).toBeInTheDocument();
expect(await canvas.findByTitle('data.json')).toBeInTheDocument();
},
};
export const CodeSectionExpanded: Story = {
args: {
code: samplePythonCode,
stdout: 'Total Revenue: $101,900.00',
stderr: '',
exitCode: 0,
isRunning: false,
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
// Click to expand the code section
const codeHeader = await canvas.findByText('Code');
await userEvent.click(codeHeader);
// The code editor should now be visible
expect(await canvas.findByText('Completed')).toBeVisible();
},
};
export const EmptyOutput: Story = {
args: {
code: `x = 1 + 1
y = x * 2
# No print statements`,
stdout: '',
stderr: '',
exitCode: 0,
isRunning: false,
},
};
export const LongOutput: Story = {
args: {
code: `for i in range(50):
print(f"Processing item {i+1}...")`,
stdout: Array.from(
{ length: 50 },
(_, i) => `Processing item ${i + 1}...`,
).join('\n'),
stderr: '',
exitCode: 0,
isRunning: false,
},
};

View file

@ -0,0 +1,146 @@
import { type Meta, type StoryObj } from '@storybook/react';
import { expect, userEvent, within } from '@storybook/test';
import { ComponentDecorator } from 'twenty-ui/testing';
import { TerminalOutput } from '@/ai/components/TerminalOutput';
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
const meta: Meta<typeof TerminalOutput> = {
title: 'Modules/AI/TerminalOutput',
component: TerminalOutput,
decorators: [I18nFrontDecorator, SnackBarDecorator, ComponentDecorator],
parameters: {
container: { width: 500 },
},
args: {
stdout: '',
stderr: '',
isRunning: false,
},
};
export default meta;
type Story = StoryObj<typeof TerminalOutput>;
export const StdoutOnly: Story = {
args: {
stdout: `Loading data from database...
Processing 1,234 records...
Applying transformations...
Total revenue: $542,890.00
Average order value: $127.50
Export complete!`,
stderr: '',
isRunning: false,
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
expect(await canvas.findByText(/Total revenue/)).toBeVisible();
expect(await canvas.findByText('stdout')).toBeVisible();
},
};
export const WithStderr: Story = {
args: {
stdout: 'Starting process...\nStep 1 complete.',
stderr:
'Warning: Deprecated function used at line 15\nError: Connection timeout after 30s',
isRunning: false,
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
// Should show stdout by default
expect(await canvas.findByText(/Starting process/)).toBeVisible();
// Click stderr tab to switch
const stderrTab = await canvas.findByText('stderr');
await userEvent.click(stderrTab);
// Should now show stderr content
expect(await canvas.findByText(/Connection timeout/)).toBeVisible();
},
};
export const StderrOnlyAutoSwitch: Story = {
args: {
stdout: '',
stderr:
'FileNotFoundError: [Errno 2] No such file or directory: \'data.csv\'\nTraceback (most recent call last):\n File "script.py", line 5, in <module>',
isRunning: false,
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
// When only stderr exists and no stdout, should auto-switch to stderr
expect(await canvas.findByText(/FileNotFoundError/)).toBeVisible();
},
};
export const Running: Story = {
args: {
stdout: 'Initializing...\nConnecting to server...',
stderr: '',
isRunning: true,
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
expect(await canvas.findByText(/Connecting to server/)).toBeVisible();
},
};
export const RunningEmpty: Story = {
args: {
stdout: '',
stderr: '',
isRunning: true,
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
expect(await canvas.findByText('Waiting for output...')).toBeVisible();
},
};
export const Empty: Story = {
args: {
stdout: '',
stderr: '',
isRunning: false,
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
expect(await canvas.findByText('No output')).toBeVisible();
},
};
export const LongOutput: Story = {
args: {
stdout: Array.from(
{ length: 100 },
(_, i) =>
`[${new Date().toISOString()}] Processing batch ${i + 1}/100...`,
).join('\n'),
stderr: '',
isRunning: false,
},
};
export const MultilineFormatted: Story = {
args: {
stdout: `╔════════════════════════════════════╗
SALES REPORT - Q4 2024
Region Revenue Growth
North $125,000 +12.5%
South $98,500 +8.2%
East $142,300 +15.1%
West $89,200 +5.7%
Total Revenue: $455,000
YoY Growth: +10.4%`,
stderr: '',
isRunning: false,
},
};

View file

@ -4,7 +4,8 @@ import { useFileCategoryColors } from '@/file/hooks/useFileCategoryColors';
import { IconMapping } from '@/file/utils/fileIconMappings';
import { useTheme } from '@emotion/react';
import { type FileUIPart } from 'ai';
import { AvatarChip, Chip, ChipVariant } from 'twenty-ui/components';
import { isDefined } from 'twenty-shared/utils';
import { AvatarChip, Chip, ChipVariant, LinkChip } from 'twenty-ui/components';
import { type IconComponent, IconX } from 'twenty-ui/display';
import { Loader } from 'twenty-ui/feedback';
@ -24,35 +25,51 @@ export const AgentChatFilePreview = ({
const fileName =
file instanceof File ? file.name : (file.filename ?? 'Unknown file');
const fileUrl = file instanceof File ? undefined : file.url;
const fileCategory: AttachmentFileCategory = getFileType(fileName);
const FileCategoryIcon: IconComponent = IconMapping[fileCategory];
const iconBackgroundColor: string = iconColors[fileCategory];
const leftComponent = isUploading ? (
<Loader color="yellow" />
) : (
<AvatarChip
Icon={FileCategoryIcon}
IconBackgroundColor={iconBackgroundColor}
/>
);
const rightComponent = onRemove ? (
<AvatarChip
Icon={IconX}
IconColor={theme.font.color.secondary}
onClick={onRemove}
divider="left"
/>
) : undefined;
if (isDefined(fileUrl)) {
return (
<LinkChip
label={fileName}
variant={ChipVariant.Static}
to={fileUrl}
target="_blank"
leftComponent={leftComponent}
rightComponent={rightComponent}
/>
);
}
return (
<Chip
label={fileName}
variant={ChipVariant.Static}
leftComponent={
isUploading ? (
<Loader color="yellow" />
) : (
<AvatarChip
Icon={FileCategoryIcon}
IconBackgroundColor={iconBackgroundColor}
/>
)
}
rightComponent={
onRemove ? (
<AvatarChip
Icon={IconX}
IconColor={theme.font.color.secondary}
onClick={onRemove}
divider="left"
/>
) : undefined
}
clickable={false}
leftComponent={leftComponent}
rightComponent={rightComponent}
/>
);
};

View file

@ -28,6 +28,7 @@
"@blocknote/server-util": "^0.31.1",
"@clickhouse/client": "^1.11.0",
"@dagrejs/dagre": "^1.1.2",
"@e2b/code-interpreter": "^1.0.4",
"@envelop/core": "4.0.3",
"@envelop/on-resolve": "4.1.0",
"@esbuild-plugins/node-modules-polyfill": "^0.2.2",

View file

@ -10,6 +10,7 @@ import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/featu
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
import { ToolCategory } from 'src/engine/core-modules/tool-provider/enums/tool-category.enum';
import { ToolProviderService } from 'src/engine/core-modules/tool-provider/services/tool-provider.service';
import { ToolType } from 'src/engine/core-modules/tool/enums/tool-type.enum';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service';
@ -134,6 +135,9 @@ export class McpProtocolService {
categories: [ToolCategory.DATABASE_CRUD, ToolCategory.ACTION],
rolePermissionConfig: { unionOf: [roleId] },
wrapWithErrorContext: false,
// Exclude code_interpreter from MCP to prevent recursive execution attacks
// (code running in the sandbox could call code_interpreter via MCP)
excludeTools: [ToolType.CODE_INTERPRETER],
});
if (method === 'tools/call' && params) {

View file

@ -0,0 +1,53 @@
import { NodeEnvironment } from 'src/engine/core-modules/twenty-config/interfaces/node-environment.interface';
import { type TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
import {
CodeInterpreterDriverType,
type CodeInterpreterModuleOptions,
} from './code-interpreter.interface';
export const codeInterpreterModuleFactory = async (
twentyConfigService: TwentyConfigService,
): Promise<CodeInterpreterModuleOptions> => {
const driverType = twentyConfigService.get('CODE_INTERPRETER_TYPE');
const timeoutMs = twentyConfigService.get('CODE_INTERPRETER_TIMEOUT_MS');
switch (driverType) {
case CodeInterpreterDriverType.LOCAL: {
const nodeEnv = twentyConfigService.get('NODE_ENV');
if (nodeEnv === NodeEnvironment.PRODUCTION) {
throw new Error(
'LOCAL code interpreter driver is not allowed in production. Use E2B driver instead by setting CODE_INTERPRETER_TYPE=E2B and providing E2B_API_KEY.',
);
}
return {
type: CodeInterpreterDriverType.LOCAL,
options: { timeoutMs },
};
}
case CodeInterpreterDriverType.E2B: {
const apiKey = twentyConfigService.get('E2B_API_KEY');
if (!apiKey) {
throw new Error(
'E2B_API_KEY is required when CODE_INTERPRETER_TYPE is E2B',
);
}
return {
type: CodeInterpreterDriverType.E2B,
options: {
apiKey,
timeoutMs,
},
};
}
default:
throw new Error(
`Invalid code interpreter driver type (${driverType}), check your .env file`,
);
}
};

View file

@ -0,0 +1,3 @@
export const CODE_INTERPRETER_DRIVER = Symbol('CODE_INTERPRETER_DRIVER');
export const DEFAULT_CODE_INTERPRETER_TIMEOUT_MS = 300_000;

View file

@ -0,0 +1,30 @@
import { type FactoryProvider, type ModuleMetadata } from '@nestjs/common';
import { type E2BDriverOptions } from './drivers/e2b.driver';
import { type LocalDriverOptions } from './drivers/local.driver';
export enum CodeInterpreterDriverType {
LOCAL = 'LOCAL',
E2B = 'E2B',
}
export type LocalDriverFactoryOptions = {
type: CodeInterpreterDriverType.LOCAL;
options: LocalDriverOptions;
};
export type E2BDriverFactoryOptions = {
type: CodeInterpreterDriverType.E2B;
options: E2BDriverOptions;
};
export type CodeInterpreterModuleOptions =
| LocalDriverFactoryOptions
| E2BDriverFactoryOptions;
export type CodeInterpreterModuleAsyncOptions = {
useFactory: (
...args: unknown[]
) => CodeInterpreterModuleOptions | Promise<CodeInterpreterModuleOptions>;
} & Pick<ModuleMetadata, 'imports'> &
Pick<FactoryProvider, 'inject'>;

View file

@ -0,0 +1,37 @@
import { type DynamicModule, Global } from '@nestjs/common';
import { CODE_INTERPRETER_DRIVER } from './code-interpreter.constants';
import {
CodeInterpreterDriverType,
type CodeInterpreterModuleAsyncOptions,
} from './code-interpreter.interface';
import { CodeInterpreterService } from './code-interpreter.service';
import { E2BDriver } from './drivers/e2b.driver';
import { LocalDriver } from './drivers/local.driver';
@Global()
export class CodeInterpreterModule {
static forRootAsync(
options: CodeInterpreterModuleAsyncOptions,
): DynamicModule {
const provider = {
provide: CODE_INTERPRETER_DRIVER,
useFactory: async (...args: unknown[]) => {
const config = await options.useFactory(...args);
return config.type === CodeInterpreterDriverType.LOCAL
? new LocalDriver(config.options)
: new E2BDriver(config.options);
},
inject: options.inject ?? [],
};
return {
module: CodeInterpreterModule,
imports: options.imports ?? [],
providers: [CodeInterpreterService, provider],
exports: [CodeInterpreterService],
};
}
}

View file

@ -0,0 +1,27 @@
import { Inject, Injectable } from '@nestjs/common';
import { CODE_INTERPRETER_DRIVER } from './code-interpreter.constants';
import {
type CodeExecutionResult,
type CodeInterpreterDriver,
type ExecutionContext,
type InputFile,
type StreamCallbacks,
} from './drivers/interfaces/code-interpreter-driver.interface';
@Injectable()
export class CodeInterpreterService implements CodeInterpreterDriver {
constructor(
@Inject(CODE_INTERPRETER_DRIVER) private driver: CodeInterpreterDriver,
) {}
execute(
code: string,
files?: InputFile[],
context?: ExecutionContext,
callbacks?: StreamCallbacks,
): Promise<CodeExecutionResult> {
return this.driver.execute(code, files, context, callbacks);
}
}

View file

@ -0,0 +1,147 @@
import { promises as fs } from 'fs';
import { join } from 'path';
import { Sandbox } from '@e2b/code-interpreter';
import { DEFAULT_CODE_INTERPRETER_TIMEOUT_MS } from 'src/engine/core-modules/code-interpreter/code-interpreter.constants';
import { getMimeType } from 'src/engine/core-modules/code-interpreter/utils/get-mime-type.util';
import {
type CodeExecutionResult,
type CodeInterpreterDriver,
type ExecutionContext,
type InputFile,
type OutputFile,
type StreamCallbacks,
} from './interfaces/code-interpreter-driver.interface';
export type E2BDriverOptions = {
apiKey: string;
timeoutMs?: number;
};
const SANDBOX_SCRIPTS_PATH = join(__dirname, '..', 'sandbox-scripts');
async function uploadDirectoryToSandbox(
sbx: Sandbox,
localPath: string,
remotePath: string,
) {
const entries = await fs.readdir(localPath, { withFileTypes: true });
for (const entry of entries) {
const localEntryPath = join(localPath, entry.name);
const remoteEntryPath = `${remotePath}/${entry.name}`;
if (entry.isDirectory()) {
await uploadDirectoryToSandbox(sbx, localEntryPath, remoteEntryPath);
} else {
const content = await fs.readFile(localEntryPath);
const arrayBuffer = new Uint8Array(content).buffer;
await sbx.files.write(remoteEntryPath, arrayBuffer);
}
}
}
export class E2BDriver implements CodeInterpreterDriver {
constructor(private options: E2BDriverOptions) {}
async execute(
code: string,
files?: InputFile[],
context?: ExecutionContext,
callbacks?: StreamCallbacks,
): Promise<CodeExecutionResult> {
const sbx = await Sandbox.create({
apiKey: this.options.apiKey,
timeoutMs: this.options.timeoutMs ?? DEFAULT_CODE_INTERPRETER_TIMEOUT_MS,
});
try {
// Upload pre-installed scripts to sandbox
try {
await uploadDirectoryToSandbox(
sbx,
SANDBOX_SCRIPTS_PATH,
'/home/user/scripts',
);
} catch {
// Scripts directory might not exist
}
for (const file of files ?? []) {
const arrayBuffer = new Uint8Array(file.content).buffer;
await sbx.files.write(`/home/user/${file.filename}`, arrayBuffer);
}
const envSetup = context?.env
? `import os\n${Object.entries(context.env)
.map(([key, value]) => {
const escapedValue = value
.replace(/\\/g, '\\\\')
.replace(/'/g, "\\'")
.replace(/\n/g, '\\n')
.replace(/\r/g, '\\r');
return `os.environ['${key}'] = '${escapedValue}'`;
})
.join('\n')}\n\n`
: '';
const outputFiles: OutputFile[] = [];
let chartCounter = 0;
const execution = await sbx.runCode(envSetup + code, {
onStdout: (data) => callbacks?.onStdout?.(data.line),
onStderr: (data) => callbacks?.onStderr?.(data.line),
onResult: (result) => {
if (result.png) {
const outputFile: OutputFile = {
filename: `chart-${chartCounter++}.png`,
content: Buffer.from(result.png, 'base64'),
mimeType: 'image/png',
};
outputFiles.push(outputFile);
callbacks?.onResult?.(outputFile);
}
},
});
try {
const outputDir = await sbx.files.list('/home/user/output');
for (const file of outputDir) {
if (file.type === 'file') {
const content = await sbx.files.read(
`/home/user/output/${file.name}`,
);
const outputFile: OutputFile = {
filename: file.name,
content: Buffer.from(content),
mimeType: getMimeType(file.name),
};
outputFiles.push(outputFile);
callbacks?.onResult?.(outputFile);
}
}
} catch {
// Output directory doesn't exist - that's fine
}
return {
stdout: execution.logs.stdout.join('\n'),
stderr: execution.logs.stderr.join('\n'),
exitCode: execution.error ? 1 : 0,
files: outputFiles,
error: execution.error?.value,
};
} finally {
await sbx.kill();
}
}
}

View file

@ -0,0 +1,38 @@
export type InputFile = {
filename: string;
content: Buffer;
mimeType: string;
};
export type OutputFile = {
filename: string;
content: Buffer;
mimeType: string;
};
export type CodeExecutionResult = {
stdout: string;
stderr: string;
exitCode: number;
files: OutputFile[];
error?: string;
};
export type ExecutionContext = {
env?: Record<string, string>;
};
export type StreamCallbacks = {
onStdout?: (line: string) => void;
onStderr?: (line: string) => void;
onResult?: (result: OutputFile) => void;
};
export interface CodeInterpreterDriver {
execute(
code: string,
files?: InputFile[],
context?: ExecutionContext,
callbacks?: StreamCallbacks,
): Promise<CodeExecutionResult>;
}

View file

@ -0,0 +1,212 @@
import { spawn } from 'child_process';
import { promises as fs } from 'fs';
import { tmpdir } from 'os';
import { basename, join } from 'path';
import { DEFAULT_CODE_INTERPRETER_TIMEOUT_MS } from 'src/engine/core-modules/code-interpreter/code-interpreter.constants';
import { getMimeType } from 'src/engine/core-modules/code-interpreter/utils/get-mime-type.util';
import {
type CodeExecutionResult,
type CodeInterpreterDriver,
type ExecutionContext,
type InputFile,
type OutputFile,
type StreamCallbacks,
} from './interfaces/code-interpreter-driver.interface';
export type LocalDriverOptions = {
timeoutMs?: number;
};
const SANDBOX_SCRIPTS_PATH = join(__dirname, '..', 'sandbox-scripts');
async function copyDirectoryRecursive(src: string, dest: string) {
await fs.mkdir(dest, { recursive: true });
const entries = await fs.readdir(src, { withFileTypes: true });
for (const entry of entries) {
const srcPath = join(src, entry.name);
const destPath = join(dest, entry.name);
if (entry.isDirectory()) {
await copyDirectoryRecursive(srcPath, destPath);
} else {
await fs.copyFile(srcPath, destPath);
}
}
}
// WARNING: This driver is UNSAFE and should only be used for development.
// It executes arbitrary Python code on the server without any sandboxing.
export class LocalDriver implements CodeInterpreterDriver {
constructor(private options: LocalDriverOptions = {}) {}
async execute(
code: string,
files?: InputFile[],
context?: ExecutionContext,
callbacks?: StreamCallbacks,
): Promise<CodeExecutionResult> {
const workDir = await fs.mkdtemp(join(tmpdir(), 'code-interpreter-'));
const outputDir = join(workDir, 'output');
const scriptsDir = join(workDir, 'scripts');
await fs.mkdir(outputDir);
// Copy pre-installed scripts to sandbox
try {
await copyDirectoryRecursive(SANDBOX_SCRIPTS_PATH, scriptsDir);
} catch {
// Scripts directory might not exist in dev environment
}
try {
for (const file of files ?? []) {
const safeFilename = basename(file.filename);
await fs.writeFile(join(workDir, safeFilename), file.content);
}
// Rewrite E2B-style paths to local paths for compatibility
const rewrittenCode = code
.replace(/\/home\/user\/scripts\//g, `${scriptsDir}/`)
.replace(/\/home\/user\/scripts/g, scriptsDir)
.replace(/\/home\/user\/output\//g, `${outputDir}/`)
.replace(/\/home\/user\/output/g, outputDir)
.replace(/\/home\/user\//g, `${workDir}/`)
.replace(/\/home\/user/g, workDir);
const scriptPath = join(workDir, 'script.py');
await fs.writeFile(scriptPath, rewrittenCode);
const timeoutMs =
this.options.timeoutMs ?? DEFAULT_CODE_INTERPRETER_TIMEOUT_MS;
const { stdout, stderr, exitCode, error } = await this.runPythonScript(
scriptPath,
workDir,
outputDir,
context?.env,
timeoutMs,
callbacks,
);
const outputFiles: OutputFile[] = [];
try {
const outputEntries = await fs.readdir(outputDir, {
withFileTypes: true,
});
for (const entry of outputEntries) {
if (entry.isFile()) {
const content = await fs.readFile(join(outputDir, entry.name));
const outputFile: OutputFile = {
filename: entry.name,
content,
mimeType: getMimeType(entry.name),
};
outputFiles.push(outputFile);
callbacks?.onResult?.(outputFile);
}
}
} catch {
// Output directory might be empty or not exist
}
return {
stdout,
stderr,
exitCode,
files: outputFiles,
error,
};
} finally {
await fs.rm(workDir, { recursive: true, force: true });
}
}
private runPythonScript(
scriptPath: string,
workDir: string,
outputDir: string,
env?: Record<string, string>,
timeoutMs?: number,
callbacks?: StreamCallbacks,
): Promise<{
stdout: string;
stderr: string;
exitCode: number;
error?: string;
}> {
return new Promise((resolve) => {
const child = spawn('python3', [scriptPath], {
cwd: workDir,
env: {
...process.env,
OUTPUT_DIR: outputDir,
...env,
},
});
let stdout = '';
let stderr = '';
let killed = false;
const timeout = setTimeout(() => {
killed = true;
child.kill('SIGKILL');
}, timeoutMs ?? DEFAULT_CODE_INTERPRETER_TIMEOUT_MS);
child.stdout.on('data', (data: Buffer) => {
const text = data.toString();
stdout += text;
const lines = text.split('\n');
for (const line of lines) {
if (line) {
callbacks?.onStdout?.(line);
}
}
});
child.stderr.on('data', (data: Buffer) => {
const text = data.toString();
stderr += text;
const lines = text.split('\n');
for (const line of lines) {
if (line) {
callbacks?.onStderr?.(line);
}
}
});
child.on('close', (code) => {
clearTimeout(timeout);
resolve({
stdout,
stderr,
exitCode: code ?? 0,
error: killed ? 'Process timed out' : undefined,
});
});
child.on('error', (err) => {
clearTimeout(timeout);
resolve({
stdout,
stderr,
exitCode: 1,
error: err.message,
});
});
});
}
}

View file

@ -0,0 +1,159 @@
#!/usr/bin/env python3
"""
Tool to pack a directory into a .docx, .pptx, or .xlsx file with XML formatting undone.
Example usage:
python pack.py <input_directory> <office_file> [--force]
"""
import argparse
import shutil
import subprocess
import sys
import tempfile
import defusedxml.minidom
import zipfile
from pathlib import Path
def main():
parser = argparse.ArgumentParser(description="Pack a directory into an Office file")
parser.add_argument("input_directory", help="Unpacked Office document directory")
parser.add_argument("output_file", help="Output Office file (.docx/.pptx/.xlsx)")
parser.add_argument("--force", action="store_true", help="Skip validation")
args = parser.parse_args()
try:
success = pack_document(
args.input_directory, args.output_file, validate=not args.force
)
# Show warning if validation was skipped
if args.force:
print("Warning: Skipped validation, file may be corrupt", file=sys.stderr)
# Exit with error if validation failed
elif not success:
print("Contents would produce a corrupt file.", file=sys.stderr)
print("Please validate XML before repacking.", file=sys.stderr)
print("Use --force to skip validation and pack anyway.", file=sys.stderr)
sys.exit(1)
except ValueError as e:
sys.exit(f"Error: {e}")
def pack_document(input_dir, output_file, validate=False):
"""Pack a directory into an Office file (.docx/.pptx/.xlsx).
Args:
input_dir: Path to unpacked Office document directory
output_file: Path to output Office file
validate: If True, validates with soffice (default: False)
Returns:
bool: True if successful, False if validation failed
"""
input_dir = Path(input_dir)
output_file = Path(output_file)
if not input_dir.is_dir():
raise ValueError(f"{input_dir} is not a directory")
if output_file.suffix.lower() not in {".docx", ".pptx", ".xlsx"}:
raise ValueError(f"{output_file} must be a .docx, .pptx, or .xlsx file")
# Work in temporary directory to avoid modifying original
with tempfile.TemporaryDirectory() as temp_dir:
temp_content_dir = Path(temp_dir) / "content"
shutil.copytree(input_dir, temp_content_dir)
# Process XML files to remove pretty-printing whitespace
for pattern in ["*.xml", "*.rels"]:
for xml_file in temp_content_dir.rglob(pattern):
condense_xml(xml_file)
# Create final Office file as zip archive
output_file.parent.mkdir(parents=True, exist_ok=True)
with zipfile.ZipFile(output_file, "w", zipfile.ZIP_DEFLATED) as zf:
for f in temp_content_dir.rglob("*"):
if f.is_file():
zf.write(f, f.relative_to(temp_content_dir))
# Validate if requested
if validate:
if not validate_document(output_file):
output_file.unlink() # Delete the corrupt file
return False
return True
def validate_document(doc_path):
"""Validate document by converting to HTML with soffice."""
# Determine the correct filter based on file extension
match doc_path.suffix.lower():
case ".docx":
filter_name = "html:HTML"
case ".pptx":
filter_name = "html:impress_html_Export"
case ".xlsx":
filter_name = "html:HTML (StarCalc)"
with tempfile.TemporaryDirectory() as temp_dir:
try:
result = subprocess.run(
[
"soffice",
"--headless",
"--convert-to",
filter_name,
"--outdir",
temp_dir,
str(doc_path),
],
capture_output=True,
timeout=10,
text=True,
)
if not (Path(temp_dir) / f"{doc_path.stem}.html").exists():
error_msg = result.stderr.strip() or "Document validation failed"
print(f"Validation error: {error_msg}", file=sys.stderr)
return False
return True
except FileNotFoundError:
print("Warning: soffice not found. Skipping validation.", file=sys.stderr)
return True
except subprocess.TimeoutExpired:
print("Validation error: Timeout during conversion", file=sys.stderr)
return False
except Exception as e:
print(f"Validation error: {e}", file=sys.stderr)
return False
def condense_xml(xml_file):
"""Strip unnecessary whitespace and remove comments."""
with open(xml_file, "r", encoding="utf-8") as f:
dom = defusedxml.minidom.parse(f)
# Process each element to remove whitespace and comments
for element in dom.getElementsByTagName("*"):
# Skip w:t elements and their processing
if element.tagName.endswith(":t"):
continue
# Remove whitespace-only text nodes and comment nodes
for child in list(element.childNodes):
if (
child.nodeType == child.TEXT_NODE
and child.nodeValue
and child.nodeValue.strip() == ""
) or child.nodeType == child.COMMENT_NODE:
element.removeChild(child)
# Write back the condensed XML
with open(xml_file, "wb") as f:
f.write(dom.toxml(encoding="UTF-8"))
if __name__ == "__main__":
main()

View file

@ -0,0 +1,29 @@
#!/usr/bin/env python3
"""Unpack and format XML contents of Office files (.docx, .pptx, .xlsx)"""
import random
import sys
import defusedxml.minidom
import zipfile
from pathlib import Path
# Get command line arguments
assert len(sys.argv) == 3, "Usage: python unpack.py <office_file> <output_dir>"
input_file, output_dir = sys.argv[1], sys.argv[2]
# Extract and format
output_path = Path(output_dir)
output_path.mkdir(parents=True, exist_ok=True)
zipfile.ZipFile(input_file).extractall(output_path)
# Pretty print all XML files
xml_files = list(output_path.rglob("*.xml")) + list(output_path.rglob("*.rels"))
for xml_file in xml_files:
content = xml_file.read_text(encoding="utf-8")
dom = defusedxml.minidom.parseString(content)
xml_file.write_bytes(dom.toprettyxml(indent=" ", encoding="ascii"))
# For .docx files, suggest an RSID for tracked changes
if input_file.endswith(".docx"):
suggested_rsid = "".join(random.choices("0123456789ABCDEF", k=8))
print(f"Suggested RSID for edit session: {suggested_rsid}")

View file

@ -0,0 +1,69 @@
#!/usr/bin/env python3
"""
Command line tool to validate Office document XML files against XSD schemas and tracked changes.
Usage:
python validate.py <dir> --original <original_file>
"""
import argparse
import sys
from pathlib import Path
from validation import DOCXSchemaValidator, PPTXSchemaValidator, RedliningValidator
def main():
parser = argparse.ArgumentParser(description="Validate Office document XML files")
parser.add_argument(
"unpacked_dir",
help="Path to unpacked Office document directory",
)
parser.add_argument(
"--original",
required=True,
help="Path to original file (.docx/.pptx/.xlsx)",
)
parser.add_argument(
"-v",
"--verbose",
action="store_true",
help="Enable verbose output",
)
args = parser.parse_args()
# Validate paths
unpacked_dir = Path(args.unpacked_dir)
original_file = Path(args.original)
file_extension = original_file.suffix.lower()
assert unpacked_dir.is_dir(), f"Error: {unpacked_dir} is not a directory"
assert original_file.is_file(), f"Error: {original_file} is not a file"
assert file_extension in [".docx", ".pptx", ".xlsx"], (
f"Error: {original_file} must be a .docx, .pptx, or .xlsx file"
)
# Run validations
match file_extension:
case ".docx":
validators = [DOCXSchemaValidator, RedliningValidator]
case ".pptx":
validators = [PPTXSchemaValidator]
case _:
print(f"Error: Validation not supported for file type {file_extension}")
sys.exit(1)
# Run validators
success = True
for V in validators:
validator = V(unpacked_dir, original_file, verbose=args.verbose)
if not validator.validate():
success = False
if success:
print("All validations PASSED!")
sys.exit(0 if success else 1)
if __name__ == "__main__":
main()

View file

@ -0,0 +1,15 @@
"""
Validation modules for Word document processing.
"""
from .base import BaseSchemaValidator
from .docx import DOCXSchemaValidator
from .pptx import PPTXSchemaValidator
from .redlining import RedliningValidator
__all__ = [
"BaseSchemaValidator",
"DOCXSchemaValidator",
"PPTXSchemaValidator",
"RedliningValidator",
]

View file

@ -0,0 +1,951 @@
"""
Base validator with common validation logic for document files.
"""
import re
from pathlib import Path
import lxml.etree
class BaseSchemaValidator:
"""Base validator with common validation logic for document files."""
# Elements whose 'id' attributes must be unique within their file
# Format: element_name -> (attribute_name, scope)
# scope can be 'file' (unique within file) or 'global' (unique across all files)
UNIQUE_ID_REQUIREMENTS = {
# Word elements
"comment": ("id", "file"), # Comment IDs in comments.xml
"commentrangestart": ("id", "file"), # Must match comment IDs
"commentrangeend": ("id", "file"), # Must match comment IDs
"bookmarkstart": ("id", "file"), # Bookmark start IDs
"bookmarkend": ("id", "file"), # Bookmark end IDs
# Note: ins and del (track changes) can share IDs when part of same revision
# PowerPoint elements
"sldid": ("id", "file"), # Slide IDs in presentation.xml
"sldmasterid": ("id", "global"), # Slide master IDs must be globally unique
"sldlayoutid": ("id", "global"), # Slide layout IDs must be globally unique
"cm": ("authorid", "file"), # Comment author IDs
# Excel elements
"sheet": ("sheetid", "file"), # Sheet IDs in workbook.xml
"definedname": ("id", "file"), # Named range IDs
# Drawing/Shape elements (all formats)
"cxnsp": ("id", "file"), # Connection shape IDs
"sp": ("id", "file"), # Shape IDs
"pic": ("id", "file"), # Picture IDs
"grpsp": ("id", "file"), # Group shape IDs
}
# Mapping of element names to expected relationship types
# Subclasses should override this with format-specific mappings
ELEMENT_RELATIONSHIP_TYPES = {}
# Unified schema mappings for all Office document types
SCHEMA_MAPPINGS = {
# Document type specific schemas
"word": "ISO-IEC29500-4_2016/wml.xsd", # Word documents
"ppt": "ISO-IEC29500-4_2016/pml.xsd", # PowerPoint presentations
"xl": "ISO-IEC29500-4_2016/sml.xsd", # Excel spreadsheets
# Common file types
"[Content_Types].xml": "ecma/fouth-edition/opc-contentTypes.xsd",
"app.xml": "ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd",
"core.xml": "ecma/fouth-edition/opc-coreProperties.xsd",
"custom.xml": "ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd",
".rels": "ecma/fouth-edition/opc-relationships.xsd",
# Word-specific files
"people.xml": "microsoft/wml-2012.xsd",
"commentsIds.xml": "microsoft/wml-cid-2016.xsd",
"commentsExtensible.xml": "microsoft/wml-cex-2018.xsd",
"commentsExtended.xml": "microsoft/wml-2012.xsd",
# Chart files (common across document types)
"chart": "ISO-IEC29500-4_2016/dml-chart.xsd",
# Theme files (common across document types)
"theme": "ISO-IEC29500-4_2016/dml-main.xsd",
# Drawing and media files
"drawing": "ISO-IEC29500-4_2016/dml-main.xsd",
}
# Unified namespace constants
MC_NAMESPACE = "http://schemas.openxmlformats.org/markup-compatibility/2006"
XML_NAMESPACE = "http://www.w3.org/XML/1998/namespace"
# Common OOXML namespaces used across validators
PACKAGE_RELATIONSHIPS_NAMESPACE = (
"http://schemas.openxmlformats.org/package/2006/relationships"
)
OFFICE_RELATIONSHIPS_NAMESPACE = (
"http://schemas.openxmlformats.org/officeDocument/2006/relationships"
)
CONTENT_TYPES_NAMESPACE = (
"http://schemas.openxmlformats.org/package/2006/content-types"
)
# Folders where we should clean ignorable namespaces
MAIN_CONTENT_FOLDERS = {"word", "ppt", "xl"}
# All allowed OOXML namespaces (superset of all document types)
OOXML_NAMESPACES = {
"http://schemas.openxmlformats.org/officeDocument/2006/math",
"http://schemas.openxmlformats.org/officeDocument/2006/relationships",
"http://schemas.openxmlformats.org/schemaLibrary/2006/main",
"http://schemas.openxmlformats.org/drawingml/2006/main",
"http://schemas.openxmlformats.org/drawingml/2006/chart",
"http://schemas.openxmlformats.org/drawingml/2006/chartDrawing",
"http://schemas.openxmlformats.org/drawingml/2006/diagram",
"http://schemas.openxmlformats.org/drawingml/2006/picture",
"http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing",
"http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing",
"http://schemas.openxmlformats.org/wordprocessingml/2006/main",
"http://schemas.openxmlformats.org/presentationml/2006/main",
"http://schemas.openxmlformats.org/spreadsheetml/2006/main",
"http://schemas.openxmlformats.org/officeDocument/2006/sharedTypes",
"http://www.w3.org/XML/1998/namespace",
}
def __init__(self, unpacked_dir, original_file, verbose=False):
self.unpacked_dir = Path(unpacked_dir).resolve()
self.original_file = Path(original_file)
self.verbose = verbose
# Set schemas directory
self.schemas_dir = Path(__file__).parent.parent.parent / "schemas"
# Get all XML and .rels files
patterns = ["*.xml", "*.rels"]
self.xml_files = [
f for pattern in patterns for f in self.unpacked_dir.rglob(pattern)
]
if not self.xml_files:
print(f"Warning: No XML files found in {self.unpacked_dir}")
def validate(self):
"""Run all validation checks and return True if all pass."""
raise NotImplementedError("Subclasses must implement the validate method")
def validate_xml(self):
"""Validate that all XML files are well-formed."""
errors = []
for xml_file in self.xml_files:
try:
# Try to parse the XML file
lxml.etree.parse(str(xml_file))
except lxml.etree.XMLSyntaxError as e:
errors.append(
f" {xml_file.relative_to(self.unpacked_dir)}: "
f"Line {e.lineno}: {e.msg}"
)
except Exception as e:
errors.append(
f" {xml_file.relative_to(self.unpacked_dir)}: "
f"Unexpected error: {str(e)}"
)
if errors:
print(f"FAILED - Found {len(errors)} XML violations:")
for error in errors:
print(error)
return False
else:
if self.verbose:
print("PASSED - All XML files are well-formed")
return True
def validate_namespaces(self):
"""Validate that namespace prefixes in Ignorable attributes are declared."""
errors = []
for xml_file in self.xml_files:
try:
root = lxml.etree.parse(str(xml_file)).getroot()
declared = set(root.nsmap.keys()) - {None} # Exclude default namespace
for attr_val in [
v for k, v in root.attrib.items() if k.endswith("Ignorable")
]:
undeclared = set(attr_val.split()) - declared
errors.extend(
f" {xml_file.relative_to(self.unpacked_dir)}: "
f"Namespace '{ns}' in Ignorable but not declared"
for ns in undeclared
)
except lxml.etree.XMLSyntaxError:
continue
if errors:
print(f"FAILED - {len(errors)} namespace issues:")
for error in errors:
print(error)
return False
if self.verbose:
print("PASSED - All namespace prefixes properly declared")
return True
def validate_unique_ids(self):
"""Validate that specific IDs are unique according to OOXML requirements."""
errors = []
global_ids = {} # Track globally unique IDs across all files
for xml_file in self.xml_files:
try:
root = lxml.etree.parse(str(xml_file)).getroot()
file_ids = {} # Track IDs that must be unique within this file
# Remove all mc:AlternateContent elements from the tree
mc_elements = root.xpath(
".//mc:AlternateContent", namespaces={"mc": self.MC_NAMESPACE}
)
for elem in mc_elements:
elem.getparent().remove(elem)
# Now check IDs in the cleaned tree
for elem in root.iter():
# Get the element name without namespace
tag = (
elem.tag.split("}")[-1].lower()
if "}" in elem.tag
else elem.tag.lower()
)
# Check if this element type has ID uniqueness requirements
if tag in self.UNIQUE_ID_REQUIREMENTS:
attr_name, scope = self.UNIQUE_ID_REQUIREMENTS[tag]
# Look for the specified attribute
id_value = None
for attr, value in elem.attrib.items():
attr_local = (
attr.split("}")[-1].lower()
if "}" in attr
else attr.lower()
)
if attr_local == attr_name:
id_value = value
break
if id_value is not None:
if scope == "global":
# Check global uniqueness
if id_value in global_ids:
prev_file, prev_line, prev_tag = global_ids[
id_value
]
errors.append(
f" {xml_file.relative_to(self.unpacked_dir)}: "
f"Line {elem.sourceline}: Global ID '{id_value}' in <{tag}> "
f"already used in {prev_file} at line {prev_line} in <{prev_tag}>"
)
else:
global_ids[id_value] = (
xml_file.relative_to(self.unpacked_dir),
elem.sourceline,
tag,
)
elif scope == "file":
# Check file-level uniqueness
key = (tag, attr_name)
if key not in file_ids:
file_ids[key] = {}
if id_value in file_ids[key]:
prev_line = file_ids[key][id_value]
errors.append(
f" {xml_file.relative_to(self.unpacked_dir)}: "
f"Line {elem.sourceline}: Duplicate {attr_name}='{id_value}' in <{tag}> "
f"(first occurrence at line {prev_line})"
)
else:
file_ids[key][id_value] = elem.sourceline
except (lxml.etree.XMLSyntaxError, Exception) as e:
errors.append(
f" {xml_file.relative_to(self.unpacked_dir)}: Error: {e}"
)
if errors:
print(f"FAILED - Found {len(errors)} ID uniqueness violations:")
for error in errors:
print(error)
return False
else:
if self.verbose:
print("PASSED - All required IDs are unique")
return True
def validate_file_references(self):
"""
Validate that all .rels files properly reference files and that all files are referenced.
"""
errors = []
# Find all .rels files
rels_files = list(self.unpacked_dir.rglob("*.rels"))
if not rels_files:
if self.verbose:
print("PASSED - No .rels files found")
return True
# Get all files in the unpacked directory (excluding reference files)
all_files = []
for file_path in self.unpacked_dir.rglob("*"):
if (
file_path.is_file()
and file_path.name != "[Content_Types].xml"
and not file_path.name.endswith(".rels")
): # This file is not referenced by .rels
all_files.append(file_path.resolve())
# Track all files that are referenced by any .rels file
all_referenced_files = set()
if self.verbose:
print(
f"Found {len(rels_files)} .rels files and {len(all_files)} target files"
)
# Check each .rels file
for rels_file in rels_files:
try:
# Parse relationships file
rels_root = lxml.etree.parse(str(rels_file)).getroot()
# Get the directory where this .rels file is located
rels_dir = rels_file.parent
# Find all relationships and their targets
referenced_files = set()
broken_refs = []
for rel in rels_root.findall(
".//ns:Relationship",
namespaces={"ns": self.PACKAGE_RELATIONSHIPS_NAMESPACE},
):
target = rel.get("Target")
if target and not target.startswith(
("http", "mailto:")
): # Skip external URLs
# Resolve the target path relative to the .rels file location
if rels_file.name == ".rels":
# Root .rels file - targets are relative to unpacked_dir
target_path = self.unpacked_dir / target
else:
# Other .rels files - targets are relative to their parent's parent
# e.g., word/_rels/document.xml.rels -> targets relative to word/
base_dir = rels_dir.parent
target_path = base_dir / target
# Normalize the path and check if it exists
try:
target_path = target_path.resolve()
if target_path.exists() and target_path.is_file():
referenced_files.add(target_path)
all_referenced_files.add(target_path)
else:
broken_refs.append((target, rel.sourceline))
except (OSError, ValueError):
broken_refs.append((target, rel.sourceline))
# Report broken references
if broken_refs:
rel_path = rels_file.relative_to(self.unpacked_dir)
for broken_ref, line_num in broken_refs:
errors.append(
f" {rel_path}: Line {line_num}: Broken reference to {broken_ref}"
)
except Exception as e:
rel_path = rels_file.relative_to(self.unpacked_dir)
errors.append(f" Error parsing {rel_path}: {e}")
# Check for unreferenced files (files that exist but are not referenced anywhere)
unreferenced_files = set(all_files) - all_referenced_files
if unreferenced_files:
for unref_file in sorted(unreferenced_files):
unref_rel_path = unref_file.relative_to(self.unpacked_dir)
errors.append(f" Unreferenced file: {unref_rel_path}")
if errors:
print(f"FAILED - Found {len(errors)} relationship validation errors:")
for error in errors:
print(error)
print(
"CRITICAL: These errors will cause the document to appear corrupt. "
+ "Broken references MUST be fixed, "
+ "and unreferenced files MUST be referenced or removed."
)
return False
else:
if self.verbose:
print(
"PASSED - All references are valid and all files are properly referenced"
)
return True
def validate_all_relationship_ids(self):
"""
Validate that all r:id attributes in XML files reference existing IDs
in their corresponding .rels files, and optionally validate relationship types.
"""
import lxml.etree
errors = []
# Process each XML file that might contain r:id references
for xml_file in self.xml_files:
# Skip .rels files themselves
if xml_file.suffix == ".rels":
continue
# Determine the corresponding .rels file
# For dir/file.xml, it's dir/_rels/file.xml.rels
rels_dir = xml_file.parent / "_rels"
rels_file = rels_dir / f"{xml_file.name}.rels"
# Skip if there's no corresponding .rels file (that's okay)
if not rels_file.exists():
continue
try:
# Parse the .rels file to get valid relationship IDs and their types
rels_root = lxml.etree.parse(str(rels_file)).getroot()
rid_to_type = {}
for rel in rels_root.findall(
f".//{{{self.PACKAGE_RELATIONSHIPS_NAMESPACE}}}Relationship"
):
rid = rel.get("Id")
rel_type = rel.get("Type", "")
if rid:
# Check for duplicate rIds
if rid in rid_to_type:
rels_rel_path = rels_file.relative_to(self.unpacked_dir)
errors.append(
f" {rels_rel_path}: Line {rel.sourceline}: "
f"Duplicate relationship ID '{rid}' (IDs must be unique)"
)
# Extract just the type name from the full URL
type_name = (
rel_type.split("/")[-1] if "/" in rel_type else rel_type
)
rid_to_type[rid] = type_name
# Parse the XML file to find all r:id references
xml_root = lxml.etree.parse(str(xml_file)).getroot()
# Find all elements with r:id attributes
for elem in xml_root.iter():
# Check for r:id attribute (relationship ID)
rid_attr = elem.get(f"{{{self.OFFICE_RELATIONSHIPS_NAMESPACE}}}id")
if rid_attr:
xml_rel_path = xml_file.relative_to(self.unpacked_dir)
elem_name = (
elem.tag.split("}")[-1] if "}" in elem.tag else elem.tag
)
# Check if the ID exists
if rid_attr not in rid_to_type:
errors.append(
f" {xml_rel_path}: Line {elem.sourceline}: "
f"<{elem_name}> references non-existent relationship '{rid_attr}' "
f"(valid IDs: {', '.join(sorted(rid_to_type.keys())[:5])}{'...' if len(rid_to_type) > 5 else ''})"
)
# Check if we have type expectations for this element
elif self.ELEMENT_RELATIONSHIP_TYPES:
expected_type = self._get_expected_relationship_type(
elem_name
)
if expected_type:
actual_type = rid_to_type[rid_attr]
# Check if the actual type matches or contains the expected type
if expected_type not in actual_type.lower():
errors.append(
f" {xml_rel_path}: Line {elem.sourceline}: "
f"<{elem_name}> references '{rid_attr}' which points to '{actual_type}' "
f"but should point to a '{expected_type}' relationship"
)
except Exception as e:
xml_rel_path = xml_file.relative_to(self.unpacked_dir)
errors.append(f" Error processing {xml_rel_path}: {e}")
if errors:
print(f"FAILED - Found {len(errors)} relationship ID reference errors:")
for error in errors:
print(error)
print("\nThese ID mismatches will cause the document to appear corrupt!")
return False
else:
if self.verbose:
print("PASSED - All relationship ID references are valid")
return True
def _get_expected_relationship_type(self, element_name):
"""
Get the expected relationship type for an element.
First checks the explicit mapping, then tries pattern detection.
"""
# Normalize element name to lowercase
elem_lower = element_name.lower()
# Check explicit mapping first
if elem_lower in self.ELEMENT_RELATIONSHIP_TYPES:
return self.ELEMENT_RELATIONSHIP_TYPES[elem_lower]
# Try pattern detection for common patterns
# Pattern 1: Elements ending in "Id" often expect a relationship of the prefix type
if elem_lower.endswith("id") and len(elem_lower) > 2:
# e.g., "sldId" -> "sld", "sldMasterId" -> "sldMaster"
prefix = elem_lower[:-2] # Remove "id"
# Check if this might be a compound like "sldMasterId"
if prefix.endswith("master"):
return prefix.lower()
elif prefix.endswith("layout"):
return prefix.lower()
else:
# Simple case like "sldId" -> "slide"
# Common transformations
if prefix == "sld":
return "slide"
return prefix.lower()
# Pattern 2: Elements ending in "Reference" expect a relationship of the prefix type
if elem_lower.endswith("reference") and len(elem_lower) > 9:
prefix = elem_lower[:-9] # Remove "reference"
return prefix.lower()
return None
def validate_content_types(self):
"""Validate that all content files are properly declared in [Content_Types].xml."""
errors = []
# Find [Content_Types].xml file
content_types_file = self.unpacked_dir / "[Content_Types].xml"
if not content_types_file.exists():
print("FAILED - [Content_Types].xml file not found")
return False
try:
# Parse and get all declared parts and extensions
root = lxml.etree.parse(str(content_types_file)).getroot()
declared_parts = set()
declared_extensions = set()
# Get Override declarations (specific files)
for override in root.findall(
f".//{{{self.CONTENT_TYPES_NAMESPACE}}}Override"
):
part_name = override.get("PartName")
if part_name is not None:
declared_parts.add(part_name.lstrip("/"))
# Get Default declarations (by extension)
for default in root.findall(
f".//{{{self.CONTENT_TYPES_NAMESPACE}}}Default"
):
extension = default.get("Extension")
if extension is not None:
declared_extensions.add(extension.lower())
# Root elements that require content type declaration
declarable_roots = {
"sld",
"sldLayout",
"sldMaster",
"presentation", # PowerPoint
"document", # Word
"workbook",
"worksheet", # Excel
"theme", # Common
}
# Common media file extensions that should be declared
media_extensions = {
"png": "image/png",
"jpg": "image/jpeg",
"jpeg": "image/jpeg",
"gif": "image/gif",
"bmp": "image/bmp",
"tiff": "image/tiff",
"wmf": "image/x-wmf",
"emf": "image/x-emf",
}
# Get all files in the unpacked directory
all_files = list(self.unpacked_dir.rglob("*"))
all_files = [f for f in all_files if f.is_file()]
# Check all XML files for Override declarations
for xml_file in self.xml_files:
path_str = str(xml_file.relative_to(self.unpacked_dir)).replace(
"\\", "/"
)
# Skip non-content files
if any(
skip in path_str
for skip in [".rels", "[Content_Types]", "docProps/", "_rels/"]
):
continue
try:
root_tag = lxml.etree.parse(str(xml_file)).getroot().tag
root_name = root_tag.split("}")[-1] if "}" in root_tag else root_tag
if root_name in declarable_roots and path_str not in declared_parts:
errors.append(
f" {path_str}: File with <{root_name}> root not declared in [Content_Types].xml"
)
except Exception:
continue # Skip unparseable files
# Check all non-XML files for Default extension declarations
for file_path in all_files:
# Skip XML files and metadata files (already checked above)
if file_path.suffix.lower() in {".xml", ".rels"}:
continue
if file_path.name == "[Content_Types].xml":
continue
if "_rels" in file_path.parts or "docProps" in file_path.parts:
continue
extension = file_path.suffix.lstrip(".").lower()
if extension and extension not in declared_extensions:
# Check if it's a known media extension that should be declared
if extension in media_extensions:
relative_path = file_path.relative_to(self.unpacked_dir)
errors.append(
f' {relative_path}: File with extension \'{extension}\' not declared in [Content_Types].xml - should add: <Default Extension="{extension}" ContentType="{media_extensions[extension]}"/>'
)
except Exception as e:
errors.append(f" Error parsing [Content_Types].xml: {e}")
if errors:
print(f"FAILED - Found {len(errors)} content type declaration errors:")
for error in errors:
print(error)
return False
else:
if self.verbose:
print(
"PASSED - All content files are properly declared in [Content_Types].xml"
)
return True
def validate_file_against_xsd(self, xml_file, verbose=False):
"""Validate a single XML file against XSD schema, comparing with original.
Args:
xml_file: Path to XML file to validate
verbose: Enable verbose output
Returns:
tuple: (is_valid, new_errors_set) where is_valid is True/False/None (skipped)
"""
# Resolve both paths to handle symlinks
xml_file = Path(xml_file).resolve()
unpacked_dir = self.unpacked_dir.resolve()
# Validate current file
is_valid, current_errors = self._validate_single_file_xsd(
xml_file, unpacked_dir
)
if is_valid is None:
return None, set() # Skipped
elif is_valid:
return True, set() # Valid, no errors
# Get errors from original file for this specific file
original_errors = self._get_original_file_errors(xml_file)
# Compare with original (both are guaranteed to be sets here)
assert current_errors is not None
new_errors = current_errors - original_errors
if new_errors:
if verbose:
relative_path = xml_file.relative_to(unpacked_dir)
print(f"FAILED - {relative_path}: {len(new_errors)} new error(s)")
for error in list(new_errors)[:3]:
truncated = error[:250] + "..." if len(error) > 250 else error
print(f" - {truncated}")
return False, new_errors
else:
# All errors existed in original
if verbose:
print(
f"PASSED - No new errors (original had {len(current_errors)} errors)"
)
return True, set()
def validate_against_xsd(self):
"""Validate XML files against XSD schemas, showing only new errors compared to original."""
new_errors = []
original_error_count = 0
valid_count = 0
skipped_count = 0
for xml_file in self.xml_files:
relative_path = str(xml_file.relative_to(self.unpacked_dir))
is_valid, new_file_errors = self.validate_file_against_xsd(
xml_file, verbose=False
)
if is_valid is None:
skipped_count += 1
continue
elif is_valid and not new_file_errors:
valid_count += 1
continue
elif is_valid:
# Had errors but all existed in original
original_error_count += 1
valid_count += 1
continue
# Has new errors
new_errors.append(f" {relative_path}: {len(new_file_errors)} new error(s)")
for error in list(new_file_errors)[:3]: # Show first 3 errors
new_errors.append(
f" - {error[:250]}..." if len(error) > 250 else f" - {error}"
)
# Print summary
if self.verbose:
print(f"Validated {len(self.xml_files)} files:")
print(f" - Valid: {valid_count}")
print(f" - Skipped (no schema): {skipped_count}")
if original_error_count:
print(f" - With original errors (ignored): {original_error_count}")
print(
f" - With NEW errors: {len(new_errors) > 0 and len([e for e in new_errors if not e.startswith(' ')]) or 0}"
)
if new_errors:
print("\nFAILED - Found NEW validation errors:")
for error in new_errors:
print(error)
return False
else:
if self.verbose:
print("\nPASSED - No new XSD validation errors introduced")
return True
def _get_schema_path(self, xml_file):
"""Determine the appropriate schema path for an XML file."""
# Check exact filename match
if xml_file.name in self.SCHEMA_MAPPINGS:
return self.schemas_dir / self.SCHEMA_MAPPINGS[xml_file.name]
# Check .rels files
if xml_file.suffix == ".rels":
return self.schemas_dir / self.SCHEMA_MAPPINGS[".rels"]
# Check chart files
if "charts/" in str(xml_file) and xml_file.name.startswith("chart"):
return self.schemas_dir / self.SCHEMA_MAPPINGS["chart"]
# Check theme files
if "theme/" in str(xml_file) and xml_file.name.startswith("theme"):
return self.schemas_dir / self.SCHEMA_MAPPINGS["theme"]
# Check if file is in a main content folder and use appropriate schema
if xml_file.parent.name in self.MAIN_CONTENT_FOLDERS:
return self.schemas_dir / self.SCHEMA_MAPPINGS[xml_file.parent.name]
return None
def _clean_ignorable_namespaces(self, xml_doc):
"""Remove attributes and elements not in allowed namespaces."""
# Create a clean copy
xml_string = lxml.etree.tostring(xml_doc, encoding="unicode")
xml_copy = lxml.etree.fromstring(xml_string)
# Remove attributes not in allowed namespaces
for elem in xml_copy.iter():
attrs_to_remove = []
for attr in elem.attrib:
# Check if attribute is from a namespace other than allowed ones
if "{" in attr:
ns = attr.split("}")[0][1:]
if ns not in self.OOXML_NAMESPACES:
attrs_to_remove.append(attr)
# Remove collected attributes
for attr in attrs_to_remove:
del elem.attrib[attr]
# Remove elements not in allowed namespaces
self._remove_ignorable_elements(xml_copy)
return lxml.etree.ElementTree(xml_copy)
def _remove_ignorable_elements(self, root):
"""Recursively remove all elements not in allowed namespaces."""
elements_to_remove = []
# Find elements to remove
for elem in list(root):
# Skip non-element nodes (comments, processing instructions, etc.)
if not hasattr(elem, "tag") or callable(elem.tag):
continue
tag_str = str(elem.tag)
if tag_str.startswith("{"):
ns = tag_str.split("}")[0][1:]
if ns not in self.OOXML_NAMESPACES:
elements_to_remove.append(elem)
continue
# Recursively clean child elements
self._remove_ignorable_elements(elem)
# Remove collected elements
for elem in elements_to_remove:
root.remove(elem)
def _preprocess_for_mc_ignorable(self, xml_doc):
"""Preprocess XML to handle mc:Ignorable attribute properly."""
# Remove mc:Ignorable attributes before validation
root = xml_doc.getroot()
# Remove mc:Ignorable attribute from root
if f"{{{self.MC_NAMESPACE}}}Ignorable" in root.attrib:
del root.attrib[f"{{{self.MC_NAMESPACE}}}Ignorable"]
return xml_doc
def _validate_single_file_xsd(self, xml_file, base_path):
"""Validate a single XML file against XSD schema. Returns (is_valid, errors_set)."""
schema_path = self._get_schema_path(xml_file)
if not schema_path:
return None, None # Skip file
try:
# Load schema
with open(schema_path, "rb") as xsd_file:
parser = lxml.etree.XMLParser()
xsd_doc = lxml.etree.parse(
xsd_file, parser=parser, base_url=str(schema_path)
)
schema = lxml.etree.XMLSchema(xsd_doc)
# Load and preprocess XML
with open(xml_file, "r") as f:
xml_doc = lxml.etree.parse(f)
xml_doc, _ = self._remove_template_tags_from_text_nodes(xml_doc)
xml_doc = self._preprocess_for_mc_ignorable(xml_doc)
# Clean ignorable namespaces if needed
relative_path = xml_file.relative_to(base_path)
if (
relative_path.parts
and relative_path.parts[0] in self.MAIN_CONTENT_FOLDERS
):
xml_doc = self._clean_ignorable_namespaces(xml_doc)
# Validate
if schema.validate(xml_doc):
return True, set()
else:
errors = set()
for error in schema.error_log:
# Store normalized error message (without line numbers for comparison)
errors.add(error.message)
return False, errors
except Exception as e:
return False, {str(e)}
def _get_original_file_errors(self, xml_file):
"""Get XSD validation errors from a single file in the original document.
Args:
xml_file: Path to the XML file in unpacked_dir to check
Returns:
set: Set of error messages from the original file
"""
import tempfile
import zipfile
# Resolve both paths to handle symlinks (e.g., /var vs /private/var on macOS)
xml_file = Path(xml_file).resolve()
unpacked_dir = self.unpacked_dir.resolve()
relative_path = xml_file.relative_to(unpacked_dir)
with tempfile.TemporaryDirectory() as temp_dir:
temp_path = Path(temp_dir)
# Extract original file
with zipfile.ZipFile(self.original_file, "r") as zip_ref:
zip_ref.extractall(temp_path)
# Find corresponding file in original
original_xml_file = temp_path / relative_path
if not original_xml_file.exists():
# File didn't exist in original, so no original errors
return set()
# Validate the specific file in original
is_valid, errors = self._validate_single_file_xsd(
original_xml_file, temp_path
)
return errors if errors else set()
def _remove_template_tags_from_text_nodes(self, xml_doc):
"""Remove template tags from XML text nodes and collect warnings.
Template tags follow the pattern {{ ... }} and are used as placeholders
for content replacement. They should be removed from text content before
XSD validation while preserving XML structure.
Returns:
tuple: (cleaned_xml_doc, warnings_list)
"""
warnings = []
template_pattern = re.compile(r"\{\{[^}]*\}\}")
# Create a copy of the document to avoid modifying the original
xml_string = lxml.etree.tostring(xml_doc, encoding="unicode")
xml_copy = lxml.etree.fromstring(xml_string)
def process_text_content(text, content_type):
if not text:
return text
matches = list(template_pattern.finditer(text))
if matches:
for match in matches:
warnings.append(
f"Found template tag in {content_type}: {match.group()}"
)
return template_pattern.sub("", text)
return text
# Process all text nodes in the document
for elem in xml_copy.iter():
# Skip processing if this is a w:t element
if not hasattr(elem, "tag") or callable(elem.tag):
continue
tag_str = str(elem.tag)
if tag_str.endswith("}t") or tag_str == "t":
continue
elem.text = process_text_content(elem.text, "text content")
elem.tail = process_text_content(elem.tail, "tail content")
return lxml.etree.ElementTree(xml_copy), warnings
if __name__ == "__main__":
raise RuntimeError("This module should not be run directly.")

View file

@ -0,0 +1,274 @@
"""
Validator for Word document XML files against XSD schemas.
"""
import re
import tempfile
import zipfile
import lxml.etree
from .base import BaseSchemaValidator
class DOCXSchemaValidator(BaseSchemaValidator):
"""Validator for Word document XML files against XSD schemas."""
# Word-specific namespace
WORD_2006_NAMESPACE = "http://schemas.openxmlformats.org/wordprocessingml/2006/main"
# Word-specific element to relationship type mappings
# Start with empty mapping - add specific cases as we discover them
ELEMENT_RELATIONSHIP_TYPES = {}
def validate(self):
"""Run all validation checks and return True if all pass."""
# Test 0: XML well-formedness
if not self.validate_xml():
return False
# Test 1: Namespace declarations
all_valid = True
if not self.validate_namespaces():
all_valid = False
# Test 2: Unique IDs
if not self.validate_unique_ids():
all_valid = False
# Test 3: Relationship and file reference validation
if not self.validate_file_references():
all_valid = False
# Test 4: Content type declarations
if not self.validate_content_types():
all_valid = False
# Test 5: XSD schema validation
if not self.validate_against_xsd():
all_valid = False
# Test 6: Whitespace preservation
if not self.validate_whitespace_preservation():
all_valid = False
# Test 7: Deletion validation
if not self.validate_deletions():
all_valid = False
# Test 8: Insertion validation
if not self.validate_insertions():
all_valid = False
# Test 9: Relationship ID reference validation
if not self.validate_all_relationship_ids():
all_valid = False
# Count and compare paragraphs
self.compare_paragraph_counts()
return all_valid
def validate_whitespace_preservation(self):
"""
Validate that w:t elements with whitespace have xml:space='preserve'.
"""
errors = []
for xml_file in self.xml_files:
# Only check document.xml files
if xml_file.name != "document.xml":
continue
try:
root = lxml.etree.parse(str(xml_file)).getroot()
# Find all w:t elements
for elem in root.iter(f"{{{self.WORD_2006_NAMESPACE}}}t"):
if elem.text:
text = elem.text
# Check if text starts or ends with whitespace
if re.match(r"^\s.*", text) or re.match(r".*\s$", text):
# Check if xml:space="preserve" attribute exists
xml_space_attr = f"{{{self.XML_NAMESPACE}}}space"
if (
xml_space_attr not in elem.attrib
or elem.attrib[xml_space_attr] != "preserve"
):
# Show a preview of the text
text_preview = (
repr(text)[:50] + "..."
if len(repr(text)) > 50
else repr(text)
)
errors.append(
f" {xml_file.relative_to(self.unpacked_dir)}: "
f"Line {elem.sourceline}: w:t element with whitespace missing xml:space='preserve': {text_preview}"
)
except (lxml.etree.XMLSyntaxError, Exception) as e:
errors.append(
f" {xml_file.relative_to(self.unpacked_dir)}: Error: {e}"
)
if errors:
print(f"FAILED - Found {len(errors)} whitespace preservation violations:")
for error in errors:
print(error)
return False
else:
if self.verbose:
print("PASSED - All whitespace is properly preserved")
return True
def validate_deletions(self):
"""
Validate that w:t elements are not within w:del elements.
For some reason, XSD validation does not catch this, so we do it manually.
"""
errors = []
for xml_file in self.xml_files:
# Only check document.xml files
if xml_file.name != "document.xml":
continue
try:
root = lxml.etree.parse(str(xml_file)).getroot()
# Find all w:t elements that are descendants of w:del elements
namespaces = {"w": self.WORD_2006_NAMESPACE}
xpath_expression = ".//w:del//w:t"
problematic_t_elements = root.xpath(
xpath_expression, namespaces=namespaces
)
for t_elem in problematic_t_elements:
if t_elem.text:
# Show a preview of the text
text_preview = (
repr(t_elem.text)[:50] + "..."
if len(repr(t_elem.text)) > 50
else repr(t_elem.text)
)
errors.append(
f" {xml_file.relative_to(self.unpacked_dir)}: "
f"Line {t_elem.sourceline}: <w:t> found within <w:del>: {text_preview}"
)
except (lxml.etree.XMLSyntaxError, Exception) as e:
errors.append(
f" {xml_file.relative_to(self.unpacked_dir)}: Error: {e}"
)
if errors:
print(f"FAILED - Found {len(errors)} deletion validation violations:")
for error in errors:
print(error)
return False
else:
if self.verbose:
print("PASSED - No w:t elements found within w:del elements")
return True
def count_paragraphs_in_unpacked(self):
"""Count the number of paragraphs in the unpacked document."""
count = 0
for xml_file in self.xml_files:
# Only check document.xml files
if xml_file.name != "document.xml":
continue
try:
root = lxml.etree.parse(str(xml_file)).getroot()
# Count all w:p elements
paragraphs = root.findall(f".//{{{self.WORD_2006_NAMESPACE}}}p")
count = len(paragraphs)
except Exception as e:
print(f"Error counting paragraphs in unpacked document: {e}")
return count
def count_paragraphs_in_original(self):
"""Count the number of paragraphs in the original docx file."""
count = 0
try:
# Create temporary directory to unpack original
with tempfile.TemporaryDirectory() as temp_dir:
# Unpack original docx
with zipfile.ZipFile(self.original_file, "r") as zip_ref:
zip_ref.extractall(temp_dir)
# Parse document.xml
doc_xml_path = temp_dir + "/word/document.xml"
root = lxml.etree.parse(doc_xml_path).getroot()
# Count all w:p elements
paragraphs = root.findall(f".//{{{self.WORD_2006_NAMESPACE}}}p")
count = len(paragraphs)
except Exception as e:
print(f"Error counting paragraphs in original document: {e}")
return count
def validate_insertions(self):
"""
Validate that w:delText elements are not within w:ins elements.
w:delText is only allowed in w:ins if nested within a w:del.
"""
errors = []
for xml_file in self.xml_files:
if xml_file.name != "document.xml":
continue
try:
root = lxml.etree.parse(str(xml_file)).getroot()
namespaces = {"w": self.WORD_2006_NAMESPACE}
# Find w:delText in w:ins that are NOT within w:del
invalid_elements = root.xpath(
".//w:ins//w:delText[not(ancestor::w:del)]",
namespaces=namespaces
)
for elem in invalid_elements:
text_preview = (
repr(elem.text or "")[:50] + "..."
if len(repr(elem.text or "")) > 50
else repr(elem.text or "")
)
errors.append(
f" {xml_file.relative_to(self.unpacked_dir)}: "
f"Line {elem.sourceline}: <w:delText> within <w:ins>: {text_preview}"
)
except (lxml.etree.XMLSyntaxError, Exception) as e:
errors.append(
f" {xml_file.relative_to(self.unpacked_dir)}: Error: {e}"
)
if errors:
print(f"FAILED - Found {len(errors)} insertion validation violations:")
for error in errors:
print(error)
return False
else:
if self.verbose:
print("PASSED - No w:delText elements within w:ins elements")
return True
def compare_paragraph_counts(self):
"""Compare paragraph counts between original and new document."""
original_count = self.count_paragraphs_in_original()
new_count = self.count_paragraphs_in_unpacked()
diff = new_count - original_count
diff_str = f"+{diff}" if diff > 0 else str(diff)
print(f"\nParagraphs: {original_count}{new_count} ({diff_str})")
if __name__ == "__main__":
raise RuntimeError("This module should not be run directly.")

View file

@ -0,0 +1,315 @@
"""
Validator for PowerPoint presentation XML files against XSD schemas.
"""
import re
from .base import BaseSchemaValidator
class PPTXSchemaValidator(BaseSchemaValidator):
"""Validator for PowerPoint presentation XML files against XSD schemas."""
# PowerPoint presentation namespace
PRESENTATIONML_NAMESPACE = (
"http://schemas.openxmlformats.org/presentationml/2006/main"
)
# PowerPoint-specific element to relationship type mappings
ELEMENT_RELATIONSHIP_TYPES = {
"sldid": "slide",
"sldmasterid": "slidemaster",
"notesmasterid": "notesmaster",
"sldlayoutid": "slidelayout",
"themeid": "theme",
"tablestyleid": "tablestyles",
}
def validate(self):
"""Run all validation checks and return True if all pass."""
# Test 0: XML well-formedness
if not self.validate_xml():
return False
# Test 1: Namespace declarations
all_valid = True
if not self.validate_namespaces():
all_valid = False
# Test 2: Unique IDs
if not self.validate_unique_ids():
all_valid = False
# Test 3: UUID ID validation
if not self.validate_uuid_ids():
all_valid = False
# Test 4: Relationship and file reference validation
if not self.validate_file_references():
all_valid = False
# Test 5: Slide layout ID validation
if not self.validate_slide_layout_ids():
all_valid = False
# Test 6: Content type declarations
if not self.validate_content_types():
all_valid = False
# Test 7: XSD schema validation
if not self.validate_against_xsd():
all_valid = False
# Test 8: Notes slide reference validation
if not self.validate_notes_slide_references():
all_valid = False
# Test 9: Relationship ID reference validation
if not self.validate_all_relationship_ids():
all_valid = False
# Test 10: Duplicate slide layout references validation
if not self.validate_no_duplicate_slide_layouts():
all_valid = False
return all_valid
def validate_uuid_ids(self):
"""Validate that ID attributes that look like UUIDs contain only hex values."""
import lxml.etree
errors = []
# UUID pattern: 8-4-4-4-12 hex digits with optional braces/hyphens
uuid_pattern = re.compile(
r"^[\{\(]?[0-9A-Fa-f]{8}-?[0-9A-Fa-f]{4}-?[0-9A-Fa-f]{4}-?[0-9A-Fa-f]{4}-?[0-9A-Fa-f]{12}[\}\)]?$"
)
for xml_file in self.xml_files:
try:
root = lxml.etree.parse(str(xml_file)).getroot()
# Check all elements for ID attributes
for elem in root.iter():
for attr, value in elem.attrib.items():
# Check if this is an ID attribute
attr_name = attr.split("}")[-1].lower()
if attr_name == "id" or attr_name.endswith("id"):
# Check if value looks like a UUID (has the right length and pattern structure)
if self._looks_like_uuid(value):
# Validate that it contains only hex characters in the right positions
if not uuid_pattern.match(value):
errors.append(
f" {xml_file.relative_to(self.unpacked_dir)}: "
f"Line {elem.sourceline}: ID '{value}' appears to be a UUID but contains invalid hex characters"
)
except (lxml.etree.XMLSyntaxError, Exception) as e:
errors.append(
f" {xml_file.relative_to(self.unpacked_dir)}: Error: {e}"
)
if errors:
print(f"FAILED - Found {len(errors)} UUID ID validation errors:")
for error in errors:
print(error)
return False
else:
if self.verbose:
print("PASSED - All UUID-like IDs contain valid hex values")
return True
def _looks_like_uuid(self, value):
"""Check if a value has the general structure of a UUID."""
# Remove common UUID delimiters
clean_value = value.strip("{}()").replace("-", "")
# Check if it's 32 hex-like characters (could include invalid hex chars)
return len(clean_value) == 32 and all(c.isalnum() for c in clean_value)
def validate_slide_layout_ids(self):
"""Validate that sldLayoutId elements in slide masters reference valid slide layouts."""
import lxml.etree
errors = []
# Find all slide master files
slide_masters = list(self.unpacked_dir.glob("ppt/slideMasters/*.xml"))
if not slide_masters:
if self.verbose:
print("PASSED - No slide masters found")
return True
for slide_master in slide_masters:
try:
# Parse the slide master file
root = lxml.etree.parse(str(slide_master)).getroot()
# Find the corresponding _rels file for this slide master
rels_file = slide_master.parent / "_rels" / f"{slide_master.name}.rels"
if not rels_file.exists():
errors.append(
f" {slide_master.relative_to(self.unpacked_dir)}: "
f"Missing relationships file: {rels_file.relative_to(self.unpacked_dir)}"
)
continue
# Parse the relationships file
rels_root = lxml.etree.parse(str(rels_file)).getroot()
# Build a set of valid relationship IDs that point to slide layouts
valid_layout_rids = set()
for rel in rels_root.findall(
f".//{{{self.PACKAGE_RELATIONSHIPS_NAMESPACE}}}Relationship"
):
rel_type = rel.get("Type", "")
if "slideLayout" in rel_type:
valid_layout_rids.add(rel.get("Id"))
# Find all sldLayoutId elements in the slide master
for sld_layout_id in root.findall(
f".//{{{self.PRESENTATIONML_NAMESPACE}}}sldLayoutId"
):
r_id = sld_layout_id.get(
f"{{{self.OFFICE_RELATIONSHIPS_NAMESPACE}}}id"
)
layout_id = sld_layout_id.get("id")
if r_id and r_id not in valid_layout_rids:
errors.append(
f" {slide_master.relative_to(self.unpacked_dir)}: "
f"Line {sld_layout_id.sourceline}: sldLayoutId with id='{layout_id}' "
f"references r:id='{r_id}' which is not found in slide layout relationships"
)
except (lxml.etree.XMLSyntaxError, Exception) as e:
errors.append(
f" {slide_master.relative_to(self.unpacked_dir)}: Error: {e}"
)
if errors:
print(f"FAILED - Found {len(errors)} slide layout ID validation errors:")
for error in errors:
print(error)
print(
"Remove invalid references or add missing slide layouts to the relationships file."
)
return False
else:
if self.verbose:
print("PASSED - All slide layout IDs reference valid slide layouts")
return True
def validate_no_duplicate_slide_layouts(self):
"""Validate that each slide has exactly one slideLayout reference."""
import lxml.etree
errors = []
slide_rels_files = list(self.unpacked_dir.glob("ppt/slides/_rels/*.xml.rels"))
for rels_file in slide_rels_files:
try:
root = lxml.etree.parse(str(rels_file)).getroot()
# Find all slideLayout relationships
layout_rels = [
rel
for rel in root.findall(
f".//{{{self.PACKAGE_RELATIONSHIPS_NAMESPACE}}}Relationship"
)
if "slideLayout" in rel.get("Type", "")
]
if len(layout_rels) > 1:
errors.append(
f" {rels_file.relative_to(self.unpacked_dir)}: has {len(layout_rels)} slideLayout references"
)
except Exception as e:
errors.append(
f" {rels_file.relative_to(self.unpacked_dir)}: Error: {e}"
)
if errors:
print("FAILED - Found slides with duplicate slideLayout references:")
for error in errors:
print(error)
return False
else:
if self.verbose:
print("PASSED - All slides have exactly one slideLayout reference")
return True
def validate_notes_slide_references(self):
"""Validate that each notesSlide file is referenced by only one slide."""
import lxml.etree
errors = []
notes_slide_references = {} # Track which slides reference each notesSlide
# Find all slide relationship files
slide_rels_files = list(self.unpacked_dir.glob("ppt/slides/_rels/*.xml.rels"))
if not slide_rels_files:
if self.verbose:
print("PASSED - No slide relationship files found")
return True
for rels_file in slide_rels_files:
try:
# Parse the relationships file
root = lxml.etree.parse(str(rels_file)).getroot()
# Find all notesSlide relationships
for rel in root.findall(
f".//{{{self.PACKAGE_RELATIONSHIPS_NAMESPACE}}}Relationship"
):
rel_type = rel.get("Type", "")
if "notesSlide" in rel_type:
target = rel.get("Target", "")
if target:
# Normalize the target path to handle relative paths
normalized_target = target.replace("../", "")
# Track which slide references this notesSlide
slide_name = rels_file.stem.replace(
".xml", ""
) # e.g., "slide1"
if normalized_target not in notes_slide_references:
notes_slide_references[normalized_target] = []
notes_slide_references[normalized_target].append(
(slide_name, rels_file)
)
except (lxml.etree.XMLSyntaxError, Exception) as e:
errors.append(
f" {rels_file.relative_to(self.unpacked_dir)}: Error: {e}"
)
# Check for duplicate references
for target, references in notes_slide_references.items():
if len(references) > 1:
slide_names = [ref[0] for ref in references]
errors.append(
f" Notes slide '{target}' is referenced by multiple slides: {', '.join(slide_names)}"
)
for slide_name, rels_file in references:
errors.append(f" - {rels_file.relative_to(self.unpacked_dir)}")
if errors:
print(
f"FAILED - Found {len([e for e in errors if not e.startswith(' ')])} notes slide reference validation errors:"
)
for error in errors:
print(error)
print("Each slide may optionally have its own slide file.")
return False
else:
if self.verbose:
print("PASSED - All notes slide references are unique")
return True
if __name__ == "__main__":
raise RuntimeError("This module should not be run directly.")

View file

@ -0,0 +1,279 @@
"""
Validator for tracked changes in Word documents.
"""
import subprocess
import tempfile
import zipfile
from pathlib import Path
class RedliningValidator:
"""Validator for tracked changes in Word documents."""
def __init__(self, unpacked_dir, original_docx, verbose=False):
self.unpacked_dir = Path(unpacked_dir)
self.original_docx = Path(original_docx)
self.verbose = verbose
self.namespaces = {
"w": "http://schemas.openxmlformats.org/wordprocessingml/2006/main"
}
def validate(self):
"""Main validation method that returns True if valid, False otherwise."""
# Verify unpacked directory exists and has correct structure
modified_file = self.unpacked_dir / "word" / "document.xml"
if not modified_file.exists():
print(f"FAILED - Modified document.xml not found at {modified_file}")
return False
# First, check if there are any tracked changes by Claude to validate
try:
import xml.etree.ElementTree as ET
tree = ET.parse(modified_file)
root = tree.getroot()
# Check for w:del or w:ins tags authored by Claude
del_elements = root.findall(".//w:del", self.namespaces)
ins_elements = root.findall(".//w:ins", self.namespaces)
# Filter to only include changes by Claude
claude_del_elements = [
elem
for elem in del_elements
if elem.get(f"{{{self.namespaces['w']}}}author") == "Claude"
]
claude_ins_elements = [
elem
for elem in ins_elements
if elem.get(f"{{{self.namespaces['w']}}}author") == "Claude"
]
# Redlining validation is only needed if tracked changes by Claude have been used.
if not claude_del_elements and not claude_ins_elements:
if self.verbose:
print("PASSED - No tracked changes by Claude found.")
return True
except Exception:
# If we can't parse the XML, continue with full validation
pass
# Create temporary directory for unpacking original docx
with tempfile.TemporaryDirectory() as temp_dir:
temp_path = Path(temp_dir)
# Unpack original docx
try:
with zipfile.ZipFile(self.original_docx, "r") as zip_ref:
zip_ref.extractall(temp_path)
except Exception as e:
print(f"FAILED - Error unpacking original docx: {e}")
return False
original_file = temp_path / "word" / "document.xml"
if not original_file.exists():
print(
f"FAILED - Original document.xml not found in {self.original_docx}"
)
return False
# Parse both XML files using xml.etree.ElementTree for redlining validation
try:
import xml.etree.ElementTree as ET
modified_tree = ET.parse(modified_file)
modified_root = modified_tree.getroot()
original_tree = ET.parse(original_file)
original_root = original_tree.getroot()
except ET.ParseError as e:
print(f"FAILED - Error parsing XML files: {e}")
return False
# Remove Claude's tracked changes from both documents
self._remove_claude_tracked_changes(original_root)
self._remove_claude_tracked_changes(modified_root)
# Extract and compare text content
modified_text = self._extract_text_content(modified_root)
original_text = self._extract_text_content(original_root)
if modified_text != original_text:
# Show detailed character-level differences for each paragraph
error_message = self._generate_detailed_diff(
original_text, modified_text
)
print(error_message)
return False
if self.verbose:
print("PASSED - All changes by Claude are properly tracked")
return True
def _generate_detailed_diff(self, original_text, modified_text):
"""Generate detailed word-level differences using git word diff."""
error_parts = [
"FAILED - Document text doesn't match after removing Claude's tracked changes",
"",
"Likely causes:",
" 1. Modified text inside another author's <w:ins> or <w:del> tags",
" 2. Made edits without proper tracked changes",
" 3. Didn't nest <w:del> inside <w:ins> when deleting another's insertion",
"",
"For pre-redlined documents, use correct patterns:",
" - To reject another's INSERTION: Nest <w:del> inside their <w:ins>",
" - To restore another's DELETION: Add new <w:ins> AFTER their <w:del>",
"",
]
# Show git word diff
git_diff = self._get_git_word_diff(original_text, modified_text)
if git_diff:
error_parts.extend(["Differences:", "============", git_diff])
else:
error_parts.append("Unable to generate word diff (git not available)")
return "\n".join(error_parts)
def _get_git_word_diff(self, original_text, modified_text):
"""Generate word diff using git with character-level precision."""
try:
with tempfile.TemporaryDirectory() as temp_dir:
temp_path = Path(temp_dir)
# Create two files
original_file = temp_path / "original.txt"
modified_file = temp_path / "modified.txt"
original_file.write_text(original_text, encoding="utf-8")
modified_file.write_text(modified_text, encoding="utf-8")
# Try character-level diff first for precise differences
result = subprocess.run(
[
"git",
"diff",
"--word-diff=plain",
"--word-diff-regex=.", # Character-by-character diff
"-U0", # Zero lines of context - show only changed lines
"--no-index",
str(original_file),
str(modified_file),
],
capture_output=True,
text=True,
)
if result.stdout.strip():
# Clean up the output - remove git diff header lines
lines = result.stdout.split("\n")
# Skip the header lines (diff --git, index, +++, ---, @@)
content_lines = []
in_content = False
for line in lines:
if line.startswith("@@"):
in_content = True
continue
if in_content and line.strip():
content_lines.append(line)
if content_lines:
return "\n".join(content_lines)
# Fallback to word-level diff if character-level is too verbose
result = subprocess.run(
[
"git",
"diff",
"--word-diff=plain",
"-U0", # Zero lines of context
"--no-index",
str(original_file),
str(modified_file),
],
capture_output=True,
text=True,
)
if result.stdout.strip():
lines = result.stdout.split("\n")
content_lines = []
in_content = False
for line in lines:
if line.startswith("@@"):
in_content = True
continue
if in_content and line.strip():
content_lines.append(line)
return "\n".join(content_lines)
except (subprocess.CalledProcessError, FileNotFoundError, Exception):
# Git not available or other error, return None to use fallback
pass
return None
def _remove_claude_tracked_changes(self, root):
"""Remove tracked changes authored by Claude from the XML root."""
ins_tag = f"{{{self.namespaces['w']}}}ins"
del_tag = f"{{{self.namespaces['w']}}}del"
author_attr = f"{{{self.namespaces['w']}}}author"
# Remove w:ins elements
for parent in root.iter():
to_remove = []
for child in parent:
if child.tag == ins_tag and child.get(author_attr) == "Claude":
to_remove.append(child)
for elem in to_remove:
parent.remove(elem)
# Unwrap content in w:del elements where author is "Claude"
deltext_tag = f"{{{self.namespaces['w']}}}delText"
t_tag = f"{{{self.namespaces['w']}}}t"
for parent in root.iter():
to_process = []
for child in parent:
if child.tag == del_tag and child.get(author_attr) == "Claude":
to_process.append((child, list(parent).index(child)))
# Process in reverse order to maintain indices
for del_elem, del_index in reversed(to_process):
# Convert w:delText to w:t before moving
for elem in del_elem.iter():
if elem.tag == deltext_tag:
elem.tag = t_tag
# Move all children of w:del to its parent before removing w:del
for child in reversed(list(del_elem)):
parent.insert(del_index, child)
parent.remove(del_elem)
def _extract_text_content(self, root):
"""Extract text content from Word XML, preserving paragraph structure.
Empty paragraphs are skipped to avoid false positives when tracked
insertions add only structural elements without text content.
"""
p_tag = f"{{{self.namespaces['w']}}}p"
t_tag = f"{{{self.namespaces['w']}}}t"
paragraphs = []
for p_elem in root.findall(f".//{p_tag}"):
# Get all text elements within this paragraph
text_parts = []
for t_elem in p_elem.findall(f".//{t_tag}"):
if t_elem.text:
text_parts.append(t_elem.text)
paragraph_text = "".join(text_parts)
# Skip empty paragraphs - they don't affect content validation
if paragraph_text:
paragraphs.append(paragraph_text)
return "\n".join(paragraphs)
if __name__ == "__main__":
raise RuntimeError("This module should not be run directly.")

View file

@ -0,0 +1,70 @@
from dataclasses import dataclass
import json
import sys
# Script to check that the `fields.json` file that Claude creates when analyzing PDFs
# does not have overlapping bounding boxes. See forms.md.
@dataclass
class RectAndField:
rect: list[float]
rect_type: str
field: dict
# Returns a list of messages that are printed to stdout for Claude to read.
def get_bounding_box_messages(fields_json_stream) -> list[str]:
messages = []
fields = json.load(fields_json_stream)
messages.append(f"Read {len(fields['form_fields'])} fields")
def rects_intersect(r1, r2):
disjoint_horizontal = r1[0] >= r2[2] or r1[2] <= r2[0]
disjoint_vertical = r1[1] >= r2[3] or r1[3] <= r2[1]
return not (disjoint_horizontal or disjoint_vertical)
rects_and_fields = []
for f in fields["form_fields"]:
rects_and_fields.append(RectAndField(f["label_bounding_box"], "label", f))
rects_and_fields.append(RectAndField(f["entry_bounding_box"], "entry", f))
has_error = False
for i, ri in enumerate(rects_and_fields):
# This is O(N^2); we can optimize if it becomes a problem.
for j in range(i + 1, len(rects_and_fields)):
rj = rects_and_fields[j]
if ri.field["page_number"] == rj.field["page_number"] and rects_intersect(ri.rect, rj.rect):
has_error = True
if ri.field is rj.field:
messages.append(f"FAILURE: intersection between label and entry bounding boxes for `{ri.field['description']}` ({ri.rect}, {rj.rect})")
else:
messages.append(f"FAILURE: intersection between {ri.rect_type} bounding box for `{ri.field['description']}` ({ri.rect}) and {rj.rect_type} bounding box for `{rj.field['description']}` ({rj.rect})")
if len(messages) >= 20:
messages.append("Aborting further checks; fix bounding boxes and try again")
return messages
if ri.rect_type == "entry":
if "entry_text" in ri.field:
font_size = ri.field["entry_text"].get("font_size", 14)
entry_height = ri.rect[3] - ri.rect[1]
if entry_height < font_size:
has_error = True
messages.append(f"FAILURE: entry bounding box height ({entry_height}) for `{ri.field['description']}` is too short for the text content (font size: {font_size}). Increase the box height or decrease the font size.")
if len(messages) >= 20:
messages.append("Aborting further checks; fix bounding boxes and try again")
return messages
if not has_error:
messages.append("SUCCESS: All bounding boxes are valid")
return messages
if __name__ == "__main__":
if len(sys.argv) != 2:
print("Usage: check_bounding_boxes.py [fields.json]")
sys.exit(1)
# Input file should be in the `fields.json` format described in forms.md.
with open(sys.argv[1]) as f:
messages = get_bounding_box_messages(f)
for msg in messages:
print(msg)

View file

@ -0,0 +1,226 @@
import unittest
import json
import io
from check_bounding_boxes import get_bounding_box_messages
# Currently this is not run automatically in CI; it's just for documentation and manual checking.
class TestGetBoundingBoxMessages(unittest.TestCase):
def create_json_stream(self, data):
"""Helper to create a JSON stream from data"""
return io.StringIO(json.dumps(data))
def test_no_intersections(self):
"""Test case with no bounding box intersections"""
data = {
"form_fields": [
{
"description": "Name",
"page_number": 1,
"label_bounding_box": [10, 10, 50, 30],
"entry_bounding_box": [60, 10, 150, 30]
},
{
"description": "Email",
"page_number": 1,
"label_bounding_box": [10, 40, 50, 60],
"entry_bounding_box": [60, 40, 150, 60]
}
]
}
stream = self.create_json_stream(data)
messages = get_bounding_box_messages(stream)
self.assertTrue(any("SUCCESS" in msg for msg in messages))
self.assertFalse(any("FAILURE" in msg for msg in messages))
def test_label_entry_intersection_same_field(self):
"""Test intersection between label and entry of the same field"""
data = {
"form_fields": [
{
"description": "Name",
"page_number": 1,
"label_bounding_box": [10, 10, 60, 30],
"entry_bounding_box": [50, 10, 150, 30] # Overlaps with label
}
]
}
stream = self.create_json_stream(data)
messages = get_bounding_box_messages(stream)
self.assertTrue(any("FAILURE" in msg and "intersection" in msg for msg in messages))
self.assertFalse(any("SUCCESS" in msg for msg in messages))
def test_intersection_between_different_fields(self):
"""Test intersection between bounding boxes of different fields"""
data = {
"form_fields": [
{
"description": "Name",
"page_number": 1,
"label_bounding_box": [10, 10, 50, 30],
"entry_bounding_box": [60, 10, 150, 30]
},
{
"description": "Email",
"page_number": 1,
"label_bounding_box": [40, 20, 80, 40], # Overlaps with Name's boxes
"entry_bounding_box": [160, 10, 250, 30]
}
]
}
stream = self.create_json_stream(data)
messages = get_bounding_box_messages(stream)
self.assertTrue(any("FAILURE" in msg and "intersection" in msg for msg in messages))
self.assertFalse(any("SUCCESS" in msg for msg in messages))
def test_different_pages_no_intersection(self):
"""Test that boxes on different pages don't count as intersecting"""
data = {
"form_fields": [
{
"description": "Name",
"page_number": 1,
"label_bounding_box": [10, 10, 50, 30],
"entry_bounding_box": [60, 10, 150, 30]
},
{
"description": "Email",
"page_number": 2,
"label_bounding_box": [10, 10, 50, 30], # Same coordinates but different page
"entry_bounding_box": [60, 10, 150, 30]
}
]
}
stream = self.create_json_stream(data)
messages = get_bounding_box_messages(stream)
self.assertTrue(any("SUCCESS" in msg for msg in messages))
self.assertFalse(any("FAILURE" in msg for msg in messages))
def test_entry_height_too_small(self):
"""Test that entry box height is checked against font size"""
data = {
"form_fields": [
{
"description": "Name",
"page_number": 1,
"label_bounding_box": [10, 10, 50, 30],
"entry_bounding_box": [60, 10, 150, 20], # Height is 10
"entry_text": {
"font_size": 14 # Font size larger than height
}
}
]
}
stream = self.create_json_stream(data)
messages = get_bounding_box_messages(stream)
self.assertTrue(any("FAILURE" in msg and "height" in msg for msg in messages))
self.assertFalse(any("SUCCESS" in msg for msg in messages))
def test_entry_height_adequate(self):
"""Test that adequate entry box height passes"""
data = {
"form_fields": [
{
"description": "Name",
"page_number": 1,
"label_bounding_box": [10, 10, 50, 30],
"entry_bounding_box": [60, 10, 150, 30], # Height is 20
"entry_text": {
"font_size": 14 # Font size smaller than height
}
}
]
}
stream = self.create_json_stream(data)
messages = get_bounding_box_messages(stream)
self.assertTrue(any("SUCCESS" in msg for msg in messages))
self.assertFalse(any("FAILURE" in msg for msg in messages))
def test_default_font_size(self):
"""Test that default font size is used when not specified"""
data = {
"form_fields": [
{
"description": "Name",
"page_number": 1,
"label_bounding_box": [10, 10, 50, 30],
"entry_bounding_box": [60, 10, 150, 20], # Height is 10
"entry_text": {} # No font_size specified, should use default 14
}
]
}
stream = self.create_json_stream(data)
messages = get_bounding_box_messages(stream)
self.assertTrue(any("FAILURE" in msg and "height" in msg for msg in messages))
self.assertFalse(any("SUCCESS" in msg for msg in messages))
def test_no_entry_text(self):
"""Test that missing entry_text doesn't cause height check"""
data = {
"form_fields": [
{
"description": "Name",
"page_number": 1,
"label_bounding_box": [10, 10, 50, 30],
"entry_bounding_box": [60, 10, 150, 20] # Small height but no entry_text
}
]
}
stream = self.create_json_stream(data)
messages = get_bounding_box_messages(stream)
self.assertTrue(any("SUCCESS" in msg for msg in messages))
self.assertFalse(any("FAILURE" in msg for msg in messages))
def test_multiple_errors_limit(self):
"""Test that error messages are limited to prevent excessive output"""
fields = []
# Create many overlapping fields
for i in range(25):
fields.append({
"description": f"Field{i}",
"page_number": 1,
"label_bounding_box": [10, 10, 50, 30], # All overlap
"entry_bounding_box": [20, 15, 60, 35] # All overlap
})
data = {"form_fields": fields}
stream = self.create_json_stream(data)
messages = get_bounding_box_messages(stream)
# Should abort after ~20 messages
self.assertTrue(any("Aborting" in msg for msg in messages))
# Should have some FAILURE messages but not hundreds
failure_count = sum(1 for msg in messages if "FAILURE" in msg)
self.assertGreater(failure_count, 0)
self.assertLess(len(messages), 30) # Should be limited
def test_edge_touching_boxes(self):
"""Test that boxes touching at edges don't count as intersecting"""
data = {
"form_fields": [
{
"description": "Name",
"page_number": 1,
"label_bounding_box": [10, 10, 50, 30],
"entry_bounding_box": [50, 10, 150, 30] # Touches at x=50
}
]
}
stream = self.create_json_stream(data)
messages = get_bounding_box_messages(stream)
self.assertTrue(any("SUCCESS" in msg for msg in messages))
self.assertFalse(any("FAILURE" in msg for msg in messages))
if __name__ == '__main__':
unittest.main()

View file

@ -0,0 +1,12 @@
import sys
from pypdf import PdfReader
# Script for Claude to run to determine whether a PDF has fillable form fields. See forms.md.
reader = PdfReader(sys.argv[1])
if (reader.get_fields()):
print("This PDF has fillable form fields")
else:
print("This PDF does not have fillable form fields; you will need to visually determine where to enter data")

View file

@ -0,0 +1,35 @@
import os
import sys
from pdf2image import convert_from_path
# Converts each page of a PDF to a PNG image.
def convert(pdf_path, output_dir, max_dim=1000):
images = convert_from_path(pdf_path, dpi=200)
for i, image in enumerate(images):
# Scale image if needed to keep width/height under `max_dim`
width, height = image.size
if width > max_dim or height > max_dim:
scale_factor = min(max_dim / width, max_dim / height)
new_width = int(width * scale_factor)
new_height = int(height * scale_factor)
image = image.resize((new_width, new_height))
image_path = os.path.join(output_dir, f"page_{i+1}.png")
image.save(image_path)
print(f"Saved page {i+1} as {image_path} (size: {image.size})")
print(f"Converted {len(images)} pages to PNG images")
if __name__ == "__main__":
if len(sys.argv) != 3:
print("Usage: convert_pdf_to_images.py [input pdf] [output directory]")
sys.exit(1)
pdf_path = sys.argv[1]
output_directory = sys.argv[2]
convert(pdf_path, output_directory)

View file

@ -0,0 +1,41 @@
import json
import sys
from PIL import Image, ImageDraw
# Creates "validation" images with rectangles for the bounding box information that
# Claude creates when determining where to add text annotations in PDFs. See forms.md.
def create_validation_image(page_number, fields_json_path, input_path, output_path):
# Input file should be in the `fields.json` format described in forms.md.
with open(fields_json_path, 'r') as f:
data = json.load(f)
img = Image.open(input_path)
draw = ImageDraw.Draw(img)
num_boxes = 0
for field in data["form_fields"]:
if field["page_number"] == page_number:
entry_box = field['entry_bounding_box']
label_box = field['label_bounding_box']
# Draw red rectangle over entry bounding box and blue rectangle over the label.
draw.rectangle(entry_box, outline='red', width=2)
draw.rectangle(label_box, outline='blue', width=2)
num_boxes += 2
img.save(output_path)
print(f"Created validation image at {output_path} with {num_boxes} bounding boxes")
if __name__ == "__main__":
if len(sys.argv) != 5:
print("Usage: create_validation_image.py [page number] [fields.json file] [input image path] [output image path]")
sys.exit(1)
page_number = int(sys.argv[1])
fields_json_path = sys.argv[2]
input_image_path = sys.argv[3]
output_image_path = sys.argv[4]
create_validation_image(page_number, fields_json_path, input_image_path, output_image_path)

View file

@ -0,0 +1,152 @@
import json
import sys
from pypdf import PdfReader
# Extracts data for the fillable form fields in a PDF and outputs JSON that
# Claude uses to fill the fields. See forms.md.
# This matches the format used by PdfReader `get_fields` and `update_page_form_field_values` methods.
def get_full_annotation_field_id(annotation):
components = []
while annotation:
field_name = annotation.get('/T')
if field_name:
components.append(field_name)
annotation = annotation.get('/Parent')
return ".".join(reversed(components)) if components else None
def make_field_dict(field, field_id):
field_dict = {"field_id": field_id}
ft = field.get('/FT')
if ft == "/Tx":
field_dict["type"] = "text"
elif ft == "/Btn":
field_dict["type"] = "checkbox" # radio groups handled separately
states = field.get("/_States_", [])
if len(states) == 2:
# "/Off" seems to always be the unchecked value, as suggested by
# https://opensource.adobe.com/dc-acrobat-sdk-docs/standards/pdfstandards/pdf/PDF32000_2008.pdf#page=448
# It can be either first or second in the "/_States_" list.
if "/Off" in states:
field_dict["checked_value"] = states[0] if states[0] != "/Off" else states[1]
field_dict["unchecked_value"] = "/Off"
else:
print(f"Unexpected state values for checkbox `${field_id}`. Its checked and unchecked values may not be correct; if you're trying to check it, visually verify the results.")
field_dict["checked_value"] = states[0]
field_dict["unchecked_value"] = states[1]
elif ft == "/Ch":
field_dict["type"] = "choice"
states = field.get("/_States_", [])
field_dict["choice_options"] = [{
"value": state[0],
"text": state[1],
} for state in states]
else:
field_dict["type"] = f"unknown ({ft})"
return field_dict
# Returns a list of fillable PDF fields:
# [
# {
# "field_id": "name",
# "page": 1,
# "type": ("text", "checkbox", "radio_group", or "choice")
# // Per-type additional fields described in forms.md
# },
# ]
def get_field_info(reader: PdfReader):
fields = reader.get_fields()
field_info_by_id = {}
possible_radio_names = set()
for field_id, field in fields.items():
# Skip if this is a container field with children, except that it might be
# a parent group for radio button options.
if field.get("/Kids"):
if field.get("/FT") == "/Btn":
possible_radio_names.add(field_id)
continue
field_info_by_id[field_id] = make_field_dict(field, field_id)
# Bounding rects are stored in annotations in page objects.
# Radio button options have a separate annotation for each choice;
# all choices have the same field name.
# See https://westhealth.github.io/exploring-fillable-forms-with-pdfrw.html
radio_fields_by_id = {}
for page_index, page in enumerate(reader.pages):
annotations = page.get('/Annots', [])
for ann in annotations:
field_id = get_full_annotation_field_id(ann)
if field_id in field_info_by_id:
field_info_by_id[field_id]["page"] = page_index + 1
field_info_by_id[field_id]["rect"] = ann.get('/Rect')
elif field_id in possible_radio_names:
try:
# ann['/AP']['/N'] should have two items. One of them is '/Off',
# the other is the active value.
on_values = [v for v in ann["/AP"]["/N"] if v != "/Off"]
except KeyError:
continue
if len(on_values) == 1:
rect = ann.get("/Rect")
if field_id not in radio_fields_by_id:
radio_fields_by_id[field_id] = {
"field_id": field_id,
"type": "radio_group",
"page": page_index + 1,
"radio_options": [],
}
# Note: at least on macOS 15.7, Preview.app doesn't show selected
# radio buttons correctly. (It does if you remove the leading slash
# from the value, but that causes them not to appear correctly in
# Chrome/Firefox/Acrobat/etc).
radio_fields_by_id[field_id]["radio_options"].append({
"value": on_values[0],
"rect": rect,
})
# Some PDFs have form field definitions without corresponding annotations,
# so we can't tell where they are. Ignore these fields for now.
fields_with_location = []
for field_info in field_info_by_id.values():
if "page" in field_info:
fields_with_location.append(field_info)
else:
print(f"Unable to determine location for field id: {field_info.get('field_id')}, ignoring")
# Sort by page number, then Y position (flipped in PDF coordinate system), then X.
def sort_key(f):
if "radio_options" in f:
rect = f["radio_options"][0]["rect"] or [0, 0, 0, 0]
else:
rect = f.get("rect") or [0, 0, 0, 0]
adjusted_position = [-rect[1], rect[0]]
return [f.get("page"), adjusted_position]
sorted_fields = fields_with_location + list(radio_fields_by_id.values())
sorted_fields.sort(key=sort_key)
return sorted_fields
def write_field_info(pdf_path: str, json_output_path: str):
reader = PdfReader(pdf_path)
field_info = get_field_info(reader)
with open(json_output_path, "w") as f:
json.dump(field_info, f, indent=2)
print(f"Wrote {len(field_info)} fields to {json_output_path}")
if __name__ == "__main__":
if len(sys.argv) != 3:
print("Usage: extract_form_field_info.py [input pdf] [output json]")
sys.exit(1)
write_field_info(sys.argv[1], sys.argv[2])

View file

@ -0,0 +1,114 @@
import json
import sys
from pypdf import PdfReader, PdfWriter
from extract_form_field_info import get_field_info
# Fills fillable form fields in a PDF. See forms.md.
def fill_pdf_fields(input_pdf_path: str, fields_json_path: str, output_pdf_path: str):
with open(fields_json_path) as f:
fields = json.load(f)
# Group by page number.
fields_by_page = {}
for field in fields:
if "value" in field:
field_id = field["field_id"]
page = field["page"]
if page not in fields_by_page:
fields_by_page[page] = {}
fields_by_page[page][field_id] = field["value"]
reader = PdfReader(input_pdf_path)
has_error = False
field_info = get_field_info(reader)
fields_by_ids = {f["field_id"]: f for f in field_info}
for field in fields:
existing_field = fields_by_ids.get(field["field_id"])
if not existing_field:
has_error = True
print(f"ERROR: `{field['field_id']}` is not a valid field ID")
elif field["page"] != existing_field["page"]:
has_error = True
print(f"ERROR: Incorrect page number for `{field['field_id']}` (got {field['page']}, expected {existing_field['page']})")
else:
if "value" in field:
err = validation_error_for_field_value(existing_field, field["value"])
if err:
print(err)
has_error = True
if has_error:
sys.exit(1)
writer = PdfWriter(clone_from=reader)
for page, field_values in fields_by_page.items():
writer.update_page_form_field_values(writer.pages[page - 1], field_values, auto_regenerate=False)
# This seems to be necessary for many PDF viewers to format the form values correctly.
# It may cause the viewer to show a "save changes" dialog even if the user doesn't make any changes.
writer.set_need_appearances_writer(True)
with open(output_pdf_path, "wb") as f:
writer.write(f)
def validation_error_for_field_value(field_info, field_value):
field_type = field_info["type"]
field_id = field_info["field_id"]
if field_type == "checkbox":
checked_val = field_info["checked_value"]
unchecked_val = field_info["unchecked_value"]
if field_value != checked_val and field_value != unchecked_val:
return f'ERROR: Invalid value "{field_value}" for checkbox field "{field_id}". The checked value is "{checked_val}" and the unchecked value is "{unchecked_val}"'
elif field_type == "radio_group":
option_values = [opt["value"] for opt in field_info["radio_options"]]
if field_value not in option_values:
return f'ERROR: Invalid value "{field_value}" for radio group field "{field_id}". Valid values are: {option_values}'
elif field_type == "choice":
choice_values = [opt["value"] for opt in field_info["choice_options"]]
if field_value not in choice_values:
return f'ERROR: Invalid value "{field_value}" for choice field "{field_id}". Valid values are: {choice_values}'
return None
# pypdf (at least version 5.7.0) has a bug when setting the value for a selection list field.
# In _writer.py around line 966:
#
# if field.get(FA.FT, "/Tx") == "/Ch" and field_flags & FA.FfBits.Combo == 0:
# txt = "\n".join(annotation.get_inherited(FA.Opt, []))
#
# The problem is that for selection lists, `get_inherited` returns a list of two-element lists like
# [["value1", "Text 1"], ["value2", "Text 2"], ...]
# This causes `join` to throw a TypeError because it expects an iterable of strings.
# The horrible workaround is to patch `get_inherited` to return a list of the value strings.
# We call the original method and adjust the return value only if the argument to `get_inherited`
# is `FA.Opt` and if the return value is a list of two-element lists.
def monkeypatch_pydpf_method():
from pypdf.generic import DictionaryObject
from pypdf.constants import FieldDictionaryAttributes
original_get_inherited = DictionaryObject.get_inherited
def patched_get_inherited(self, key: str, default = None):
result = original_get_inherited(self, key, default)
if key == FieldDictionaryAttributes.Opt:
if isinstance(result, list) and all(isinstance(v, list) and len(v) == 2 for v in result):
result = [r[0] for r in result]
return result
DictionaryObject.get_inherited = patched_get_inherited
if __name__ == "__main__":
if len(sys.argv) != 4:
print("Usage: fill_fillable_fields.py [input pdf] [field_values.json] [output pdf]")
sys.exit(1)
monkeypatch_pydpf_method()
input_pdf = sys.argv[1]
fields_json = sys.argv[2]
output_pdf = sys.argv[3]
fill_pdf_fields(input_pdf, fields_json, output_pdf)

View file

@ -0,0 +1,108 @@
import json
import sys
from pypdf import PdfReader, PdfWriter
from pypdf.annotations import FreeText
# Fills a PDF by adding text annotations defined in `fields.json`. See forms.md.
def transform_coordinates(bbox, image_width, image_height, pdf_width, pdf_height):
"""Transform bounding box from image coordinates to PDF coordinates"""
# Image coordinates: origin at top-left, y increases downward
# PDF coordinates: origin at bottom-left, y increases upward
x_scale = pdf_width / image_width
y_scale = pdf_height / image_height
left = bbox[0] * x_scale
right = bbox[2] * x_scale
# Flip Y coordinates for PDF
top = pdf_height - (bbox[1] * y_scale)
bottom = pdf_height - (bbox[3] * y_scale)
return left, bottom, right, top
def fill_pdf_form(input_pdf_path, fields_json_path, output_pdf_path):
"""Fill the PDF form with data from fields.json"""
# `fields.json` format described in forms.md.
with open(fields_json_path, "r") as f:
fields_data = json.load(f)
# Open the PDF
reader = PdfReader(input_pdf_path)
writer = PdfWriter()
# Copy all pages to writer
writer.append(reader)
# Get PDF dimensions for each page
pdf_dimensions = {}
for i, page in enumerate(reader.pages):
mediabox = page.mediabox
pdf_dimensions[i + 1] = [mediabox.width, mediabox.height]
# Process each form field
annotations = []
for field in fields_data["form_fields"]:
page_num = field["page_number"]
# Get page dimensions and transform coordinates.
page_info = next(p for p in fields_data["pages"] if p["page_number"] == page_num)
image_width = page_info["image_width"]
image_height = page_info["image_height"]
pdf_width, pdf_height = pdf_dimensions[page_num]
transformed_entry_box = transform_coordinates(
field["entry_bounding_box"],
image_width, image_height,
pdf_width, pdf_height
)
# Skip empty fields
if "entry_text" not in field or "text" not in field["entry_text"]:
continue
entry_text = field["entry_text"]
text = entry_text["text"]
if not text:
continue
font_name = entry_text.get("font", "Arial")
font_size = str(entry_text.get("font_size", 14)) + "pt"
font_color = entry_text.get("font_color", "000000")
# Font size/color seems to not work reliably across viewers:
# https://github.com/py-pdf/pypdf/issues/2084
annotation = FreeText(
text=text,
rect=transformed_entry_box,
font=font_name,
font_size=font_size,
font_color=font_color,
border_color=None,
background_color=None,
)
annotations.append(annotation)
# page_number is 0-based for pypdf
writer.add_annotation(page_number=page_num - 1, annotation=annotation)
# Save the filled PDF
with open(output_pdf_path, "wb") as output:
writer.write(output)
print(f"Successfully filled PDF form and saved to {output_pdf_path}")
print(f"Added {len(annotations)} text annotations")
if __name__ == "__main__":
if len(sys.argv) != 4:
print("Usage: fill_pdf_form_with_annotations.py [input pdf] [fields.json] [output pdf]")
sys.exit(1)
input_pdf = sys.argv[1]
fields_json = sys.argv[2]
output_pdf = sys.argv[3]
fill_pdf_form(input_pdf, fields_json, output_pdf)

View file

@ -0,0 +1,231 @@
#!/usr/bin/env python3
"""
Rearrange PowerPoint slides based on a sequence of indices.
Usage:
python rearrange.py template.pptx output.pptx 0,34,34,50,52
This will create output.pptx using slides from template.pptx in the specified order.
Slides can be repeated (e.g., 34 appears twice).
"""
import argparse
import shutil
import sys
from copy import deepcopy
from pathlib import Path
import six
from pptx import Presentation
def main():
parser = argparse.ArgumentParser(
description="Rearrange PowerPoint slides based on a sequence of indices.",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
python rearrange.py template.pptx output.pptx 0,34,34,50,52
Creates output.pptx using slides 0, 34 (twice), 50, and 52 from template.pptx
python rearrange.py template.pptx output.pptx 5,3,1,2,4
Creates output.pptx with slides reordered as specified
Note: Slide indices are 0-based (first slide is 0, second is 1, etc.)
""",
)
parser.add_argument("template", help="Path to template PPTX file")
parser.add_argument("output", help="Path for output PPTX file")
parser.add_argument(
"sequence", help="Comma-separated sequence of slide indices (0-based)"
)
args = parser.parse_args()
# Parse the slide sequence
try:
slide_sequence = [int(x.strip()) for x in args.sequence.split(",")]
except ValueError:
print(
"Error: Invalid sequence format. Use comma-separated integers (e.g., 0,34,34,50,52)"
)
sys.exit(1)
# Check template exists
template_path = Path(args.template)
if not template_path.exists():
print(f"Error: Template file not found: {args.template}")
sys.exit(1)
# Create output directory if needed
output_path = Path(args.output)
output_path.parent.mkdir(parents=True, exist_ok=True)
try:
rearrange_presentation(template_path, output_path, slide_sequence)
except ValueError as e:
print(f"Error: {e}")
sys.exit(1)
except Exception as e:
print(f"Error processing presentation: {e}")
sys.exit(1)
def duplicate_slide(pres, index):
"""Duplicate a slide in the presentation."""
source = pres.slides[index]
# Use source's layout to preserve formatting
new_slide = pres.slides.add_slide(source.slide_layout)
# Collect all image and media relationships from the source slide
image_rels = {}
for rel_id, rel in six.iteritems(source.part.rels):
if "image" in rel.reltype or "media" in rel.reltype:
image_rels[rel_id] = rel
# CRITICAL: Clear placeholder shapes to avoid duplicates
for shape in new_slide.shapes:
sp = shape.element
sp.getparent().remove(sp)
# Copy all shapes from source
for shape in source.shapes:
el = shape.element
new_el = deepcopy(el)
new_slide.shapes._spTree.insert_element_before(new_el, "p:extLst")
# Handle picture shapes - need to update the blip reference
# Look for all blip elements (they can be in pic or other contexts)
# Using the element's own xpath method without namespaces argument
blips = new_el.xpath(".//a:blip[@r:embed]")
for blip in blips:
old_rId = blip.get(
"{http://schemas.openxmlformats.org/officeDocument/2006/relationships}embed"
)
if old_rId in image_rels:
# Create a new relationship in the destination slide for this image
old_rel = image_rels[old_rId]
# get_or_add returns the rId directly, or adds and returns new rId
new_rId = new_slide.part.rels.get_or_add(
old_rel.reltype, old_rel._target
)
# Update the blip's embed reference to use the new relationship ID
blip.set(
"{http://schemas.openxmlformats.org/officeDocument/2006/relationships}embed",
new_rId,
)
# Copy any additional image/media relationships that might be referenced elsewhere
for rel_id, rel in image_rels.items():
try:
new_slide.part.rels.get_or_add(rel.reltype, rel._target)
except Exception:
pass # Relationship might already exist
return new_slide
def delete_slide(pres, index):
"""Delete a slide from the presentation."""
rId = pres.slides._sldIdLst[index].rId
pres.part.drop_rel(rId)
del pres.slides._sldIdLst[index]
def reorder_slides(pres, slide_index, target_index):
"""Move a slide from one position to another."""
slides = pres.slides._sldIdLst
# Remove slide element from current position
slide_element = slides[slide_index]
slides.remove(slide_element)
# Insert at target position
slides.insert(target_index, slide_element)
def rearrange_presentation(template_path, output_path, slide_sequence):
"""
Create a new presentation with slides from template in specified order.
Args:
template_path: Path to template PPTX file
output_path: Path for output PPTX file
slide_sequence: List of slide indices (0-based) to include
"""
# Copy template to preserve dimensions and theme
if template_path != output_path:
shutil.copy2(template_path, output_path)
prs = Presentation(output_path)
else:
prs = Presentation(template_path)
total_slides = len(prs.slides)
# Validate indices
for idx in slide_sequence:
if idx < 0 or idx >= total_slides:
raise ValueError(f"Slide index {idx} out of range (0-{total_slides - 1})")
# Track original slides and their duplicates
slide_map = [] # List of actual slide indices for final presentation
duplicated = {} # Track duplicates: original_idx -> [duplicate_indices]
# Step 1: DUPLICATE repeated slides
print(f"Processing {len(slide_sequence)} slides from template...")
for i, template_idx in enumerate(slide_sequence):
if template_idx in duplicated and duplicated[template_idx]:
# Already duplicated this slide, use the duplicate
slide_map.append(duplicated[template_idx].pop(0))
print(f" [{i}] Using duplicate of slide {template_idx}")
elif slide_sequence.count(template_idx) > 1 and template_idx not in duplicated:
# First occurrence of a repeated slide - create duplicates
slide_map.append(template_idx)
duplicates = []
count = slide_sequence.count(template_idx) - 1
print(
f" [{i}] Using original slide {template_idx}, creating {count} duplicate(s)"
)
for _ in range(count):
duplicate_slide(prs, template_idx)
duplicates.append(len(prs.slides) - 1)
duplicated[template_idx] = duplicates
else:
# Unique slide or first occurrence already handled, use original
slide_map.append(template_idx)
print(f" [{i}] Using original slide {template_idx}")
# Step 2: DELETE unwanted slides (work backwards)
slides_to_keep = set(slide_map)
print(f"\nDeleting {len(prs.slides) - len(slides_to_keep)} unused slides...")
for i in range(len(prs.slides) - 1, -1, -1):
if i not in slides_to_keep:
delete_slide(prs, i)
# Update slide_map indices after deletion
slide_map = [idx - 1 if idx > i else idx for idx in slide_map]
# Step 3: REORDER to final sequence
print(f"Reordering {len(slide_map)} slides to final sequence...")
for target_pos in range(len(slide_map)):
# Find which slide should be at target_pos
current_pos = slide_map[target_pos]
if current_pos != target_pos:
reorder_slides(prs, current_pos, target_pos)
# Update slide_map: the move shifts other slides
for i in range(len(slide_map)):
if slide_map[i] > current_pos and slide_map[i] <= target_pos:
slide_map[i] -= 1
elif slide_map[i] < current_pos and slide_map[i] >= target_pos:
slide_map[i] += 1
slide_map[target_pos] = target_pos
# Save the presentation
prs.save(output_path)
print(f"\nSaved rearranged presentation to: {output_path}")
print(f"Final presentation has {len(prs.slides)} slides")
if __name__ == "__main__":
main()

Some files were not shown because too many files have changed in this diff Show more