mirror of
https://github.com/twentyhq/twenty
synced 2026-04-21 13:37:22 +00:00
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:
parent
4281a71f40
commit
2e104c8e76
151 changed files with 10361 additions and 449 deletions
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
};
|
||||
|
|
@ -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,
|
||||
},
|
||||
};
|
||||
|
|
@ -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,
|
||||
},
|
||||
};
|
||||
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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`,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
export const CODE_INTERPRETER_DRIVER = Symbol('CODE_INTERPRETER_DRIVER');
|
||||
|
||||
export const DEFAULT_CODE_INTERPRETER_TIMEOUT_MS = 300_000;
|
||||
|
|
@ -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'>;
|
||||
|
|
@ -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],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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>;
|
||||
}
|
||||
|
|
@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
|
@ -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}")
|
||||
|
|
@ -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()
|
||||
|
|
@ -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",
|
||||
]
|
||||
|
|
@ -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.")
|
||||
|
|
@ -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.")
|
||||
|
|
@ -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.")
|
||||
|
|
@ -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.")
|
||||
|
|
@ -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)
|
||||
|
|
@ -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()
|
||||
|
|
@ -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")
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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])
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -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
Loading…
Reference in a new issue