From 9b106bf1292b711d72a8ae82a4b33007c2a883eb Mon Sep 17 00:00:00 2001 From: poduck Date: Fri, 28 Nov 2025 23:45:55 -0500 Subject: [PATCH] feat: Add plugin configuration editing with template variable parsing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Backend Changes: - Enhanced PluginTemplate.save() to auto-parse template variables from plugin code - Updated PluginInstallationSerializer to expose template metadata (description, category, version, author, logo, template_variables) - Fixed template variable parser to handle nested {{ }} braces in default values - Added brace-counting algorithm to properly extract variables with insertion codes - Fixed explicit type parameter detection (textarea, text, email, etc.) - Made scheduled_task optional on PluginInstallation model - Added EventPlugin through model for event-plugin relationships - Added Event.execute_plugins() method for plugin automation ## Frontend Changes: - Created Tasks.tsx page for managing scheduled tasks - Enhanced MyPlugins page with clickable plugin cards - Added edit configuration modal with dynamic form generation - Implemented escape sequence handling (convert \n, \', etc. for display) - Added plugin logos to My Plugins page - Updated type definitions for PluginInstallation interface - Added insertion code documentation to Plugin Docs ## Plugin System: - All platform plugins now have editable email templates with textarea support - Template variables properly parsed with full default values - Insertion codes ({{CUSTOMER_NAME}}, {{BUSINESS_NAME}}, etc.) documented - Plugin logos displayed in marketplace and My Plugins 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- frontend/package-lock.json | 300 +++++++++++++- frontend/package.json | 2 + .../appointment-reminder-24hr.png | Bin 0 -> 993 bytes .../plugin-logos/appointment_reminder.svg | 1 + .../public/plugin-logos/backup_database.svg | 1 + .../plugin-logos/birthday-greetings.png | Bin 0 -> 840 bytes .../plugin-logos/cleanup_old_events.svg | 1 + .../daily-appointment-summary.png | Bin 0 -> 1122 bytes frontend/public/plugin-logos/daily_report.svg | 1 + .../plugin-logos/generate_all_logos_gemini.py | 132 +++++++ .../plugin-logos/generate_daily_summary.py | 77 ++++ .../plugin-logos/generate_plugin_logos.py | 155 ++++++++ .../plugin-logos/generate_with_2_0_flash.py | 39 ++ .../plugin-logos/generate_with_gemini.py | 194 ++++++++++ .../plugin-logos/generate_with_gemini_sdk.py | 112 ++++++ .../plugin-logos/generate_with_imagen.py | 146 +++++++ .../inactive-customer-reengagement.png | Bin 0 -> 1351 bytes .../plugin-logos/monthly-revenue-report.png | Bin 0 -> 706 bytes .../public/plugin-logos/no-show-tracker.png | Bin 0 -> 1146 bytes frontend/public/plugin-logos/send_email.svg | 1 + .../public/plugin-logos/test_gemini_image.py | 43 +++ .../plugin-logos/test_gemini_native_image.py | 41 ++ .../public/plugin-logos/test_google_genai.py | 41 ++ frontend/public/plugin-logos/webhook.svg | 1 + frontend/src/App.tsx | 11 + frontend/src/pages/HelpPluginDocs.tsx | 77 +++- frontend/src/pages/MyPlugins.tsx | 221 ++++++++++- frontend/src/pages/PluginMarketplace.tsx | 309 +++++++++++---- frontend/src/pages/Tasks.tsx | 365 ++++++++++++++++++ frontend/src/types.ts | 6 + .../commands/seed_platform_plugins.py | 215 ++++++++++- .../0017_plugintemplate_logo_url.py | 18 + ...alter_plugininstallation_scheduled_task.py | 19 + ..._event_status_eventplugin_event_plugins.py | 40 ++ smoothschedule/schedule/models.py | 140 ++++++- smoothschedule/schedule/safe_scripting.py | 61 ++- smoothschedule/schedule/serializers.py | 40 +- smoothschedule/schedule/template_parser.py | 165 +++++++- 38 files changed, 2859 insertions(+), 116 deletions(-) create mode 100644 frontend/public/plugin-logos/appointment-reminder-24hr.png create mode 100644 frontend/public/plugin-logos/appointment_reminder.svg create mode 100644 frontend/public/plugin-logos/backup_database.svg create mode 100644 frontend/public/plugin-logos/birthday-greetings.png create mode 100644 frontend/public/plugin-logos/cleanup_old_events.svg create mode 100644 frontend/public/plugin-logos/daily-appointment-summary.png create mode 100644 frontend/public/plugin-logos/daily_report.svg create mode 100644 frontend/public/plugin-logos/generate_all_logos_gemini.py create mode 100644 frontend/public/plugin-logos/generate_daily_summary.py create mode 100644 frontend/public/plugin-logos/generate_plugin_logos.py create mode 100644 frontend/public/plugin-logos/generate_with_2_0_flash.py create mode 100644 frontend/public/plugin-logos/generate_with_gemini.py create mode 100644 frontend/public/plugin-logos/generate_with_gemini_sdk.py create mode 100644 frontend/public/plugin-logos/generate_with_imagen.py create mode 100644 frontend/public/plugin-logos/inactive-customer-reengagement.png create mode 100644 frontend/public/plugin-logos/monthly-revenue-report.png create mode 100644 frontend/public/plugin-logos/no-show-tracker.png create mode 100644 frontend/public/plugin-logos/send_email.svg create mode 100644 frontend/public/plugin-logos/test_gemini_image.py create mode 100644 frontend/public/plugin-logos/test_gemini_native_image.py create mode 100644 frontend/public/plugin-logos/test_google_genai.py create mode 100644 frontend/public/plugin-logos/webhook.svg create mode 100644 frontend/src/pages/Tasks.tsx create mode 100644 smoothschedule/schedule/migrations/0017_plugintemplate_logo_url.py create mode 100644 smoothschedule/schedule/migrations/0018_alter_plugininstallation_scheduled_task.py create mode 100644 smoothschedule/schedule/migrations/0019_alter_event_status_eventplugin_event_plugins.py diff --git a/frontend/package-lock.json b/frontend/package-lock.json index f35b032..7a0eda5 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -13,6 +13,7 @@ "@stripe/connect-js": "^3.3.31", "@stripe/react-connect-js": "^3.3.31", "@tanstack/react-query": "^5.90.10", + "@types/react-syntax-highlighter": "^15.5.13", "axios": "^1.13.2", "date-fns": "^4.1.0", "i18next": "^25.6.3", @@ -25,6 +26,7 @@ "react-i18next": "^16.3.5", "react-phone-number-input": "^3.4.14", "react-router-dom": "^7.9.6", + "react-syntax-highlighter": "^16.1.0", "recharts": "^3.5.0" }, "devDependencies": { @@ -1882,6 +1884,15 @@ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "license": "MIT" }, + "node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -1898,11 +1909,16 @@ "undici-types": "~7.16.0" } }, + "node_modules/@types/prismjs": { + "version": "1.26.5", + "resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.5.tgz", + "integrity": "sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ==", + "license": "MIT" + }, "node_modules/@types/react": { "version": "19.2.6", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.6.tgz", "integrity": "sha512-p/jUvulfgU7oKtj6Xpk8cA2Y1xKTtICGpJYeJXz2YVO2UcvjQgeRMLDGfDeqeRW2Ta+0QNFwcc8X3GH8SxZz6w==", - "devOptional": true, "license": "MIT", "dependencies": { "csstype": "^3.2.2" @@ -1918,6 +1934,21 @@ "@types/react": "^19.2.0" } }, + "node_modules/@types/react-syntax-highlighter": { + "version": "15.5.13", + "resolved": "https://registry.npmjs.org/@types/react-syntax-highlighter/-/react-syntax-highlighter-15.5.13.tgz", + "integrity": "sha512-uLGJ87j6Sz8UaBAooU0T6lWJ0dBmjZgN1PZTrj05TNql2/XpC6+4HhMT5syIdFUUt+FASfCeLLv4kBygNU+8qA==", + "license": "MIT", + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, "node_modules/@types/use-sync-external-store": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", @@ -2177,6 +2208,36 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-reference-invalid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", + "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/classnames": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", @@ -2222,6 +2283,16 @@ "node": ">= 0.8" } }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -2433,6 +2504,19 @@ "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", "license": "MIT" }, + "node_modules/decode-named-character-reference": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.2.0.tgz", + "integrity": "sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==", + "license": "MIT", + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -2824,6 +2908,19 @@ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "license": "MIT" }, + "node_modules/fault": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/fault/-/fault-1.0.4.tgz", + "integrity": "sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA==", + "license": "MIT", + "dependencies": { + "format": "^0.2.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -2925,6 +3022,14 @@ "node": ">= 6" } }, + "node_modules/format": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz", + "integrity": "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==", + "engines": { + "node": ">=0.4.x" + } + }, "node_modules/fraction.js": { "version": "5.3.4", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", @@ -3111,6 +3216,36 @@ "node": ">= 0.4" } }, + "node_modules/hast-util-parse-selector": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz", + "integrity": "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hastscript": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-9.0.1.tgz", + "integrity": "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-parse-selector": "^4.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/hermes-estree": { "version": "0.25.1", "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", @@ -3128,6 +3263,21 @@ "hermes-estree": "0.25.1" } }, + "node_modules/highlight.js": { + "version": "10.7.3", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", + "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==", + "license": "BSD-3-Clause", + "engines": { + "node": "*" + } + }, + "node_modules/highlightjs-vue": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/highlightjs-vue/-/highlightjs-vue-1.0.0.tgz", + "integrity": "sha512-PDEfEF102G23vHmPhLyPboFCD+BkMGu+GuJe2d9/eH4FsCwvgBpnc9n0pGE+ffKdph38s6foEZiEjdgHdzp+IA==", + "license": "CC0-1.0" + }, "node_modules/html-parse-stringify": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", @@ -3260,6 +3410,40 @@ "node": ">=12" } }, + "node_modules/is-alphabetical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", + "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", + "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", + "license": "MIT", + "dependencies": { + "is-alphabetical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-decimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", + "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -3281,6 +3465,16 @@ "node": ">=0.10.0" } }, + "node_modules/is-hexadecimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", + "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -3681,6 +3875,20 @@ "loose-envify": "cli.js" } }, + "node_modules/lowlight": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/lowlight/-/lowlight-1.20.0.tgz", + "integrity": "sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw==", + "license": "MIT", + "dependencies": { + "fault": "^1.0.0", + "highlight.js": "~10.7.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -3888,6 +4096,31 @@ "node": ">=6" } }, + "node_modules/parse-entities": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", + "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "character-entities-legacy": "^3.0.0", + "character-reference-invalid": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0", + "is-hexadecimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/parse-entities/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -4003,6 +4236,15 @@ "node": ">= 0.8.0" } }, + "node_modules/prismjs": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz", + "integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -4020,6 +4262,16 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "license": "MIT" }, + "node_modules/property-information": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", + "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", @@ -4195,6 +4447,26 @@ "react-dom": ">=18" } }, + "node_modules/react-syntax-highlighter": { + "version": "16.1.0", + "resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-16.1.0.tgz", + "integrity": "sha512-E40/hBiP5rCNwkeBN1vRP+xow1X0pndinO+z3h7HLsHyjztbyjfzNWNKuAsJj+7DLam9iT4AaaOZnueCU+Nplg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4", + "highlight.js": "^10.4.1", + "highlightjs-vue": "^1.0.0", + "lowlight": "^1.17.0", + "prismjs": "^1.30.0", + "refractor": "^5.0.0" + }, + "engines": { + "node": ">= 16.20.2" + }, + "peerDependencies": { + "react": ">= 0.14.0" + } + }, "node_modules/recharts": { "version": "3.5.0", "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.5.0.tgz", @@ -4241,6 +4513,22 @@ "redux": "^5.0.0" } }, + "node_modules/refractor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/refractor/-/refractor-5.0.0.tgz", + "integrity": "sha512-QXOrHQF5jOpjjLfiNk5GFnWhRXvxjUVnlFxkeDmewR5sXkr3iM46Zo+CnRR8B+MDVqkULW4EcLVcRBNOPXHosw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/prismjs": "^1.0.0", + "hastscript": "^9.0.0", + "parse-entities": "^4.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/reselect": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", @@ -4351,6 +4639,16 @@ "node": ">=0.10.0" } }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index 9daac16..7a78d40 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -9,6 +9,7 @@ "@stripe/connect-js": "^3.3.31", "@stripe/react-connect-js": "^3.3.31", "@tanstack/react-query": "^5.90.10", + "@types/react-syntax-highlighter": "^15.5.13", "axios": "^1.13.2", "date-fns": "^4.1.0", "i18next": "^25.6.3", @@ -21,6 +22,7 @@ "react-i18next": "^16.3.5", "react-phone-number-input": "^3.4.14", "react-router-dom": "^7.9.6", + "react-syntax-highlighter": "^16.1.0", "recharts": "^3.5.0" }, "devDependencies": { diff --git a/frontend/public/plugin-logos/appointment-reminder-24hr.png b/frontend/public/plugin-logos/appointment-reminder-24hr.png new file mode 100644 index 0000000000000000000000000000000000000000..5062e76fb9fb5e0225ce0b71b60a99bb5e5adc3f GIT binary patch literal 993 zcmeAS@N?(olHy`uVBq!ia0vp^6F``Q4M;wBd$farfq8|ei(^Q|oVRza%Y+?8TrW-% z6a2&y(ZXk=XT#NYK;uoJrb3D#)P9_T47<3D!iTcONv_XX!m)pHUj2neXiGj6P1=yIJ~m}8Q& zmyi6J<19@r4GM}19({BEI~;^@I5>oupyF7VKev1rF8K8G_WU=u_wBV?^TX#+hSh>e z3CrHy&I<5lThd}&dghu_mf8Z-MKi1Bh$Uq`QmI;95hGXb9qO;p&MI^3R^9&}Ru%L0 z@7J&WHD_AMim&ajWFPJP`t6s2)VZ}BilIAOEsssU^y5n8qi@wK^c1dF)Y;jd-8H}3 zC6qNJ+D7(y$12Vh*D9>m35E(z$hOhtke^U&k3xl#f(MrH z0fr{gqMU*Oy8ExiXdSvPtEBK_6Tilx-37ky;`i4&d`Od>@OyW@_PO=Vr4Ap`r6+ve zoyWg!eS5A$$3Y{7>*cra$;6AFwl6IIUB70%Z{&=%^H)os=4`Q;#qV)fOlJF3T~m1h z6BZUuMn@-y2`2Irei_c`|JcybaOsccrC(n>Pk!&*pnGSUUS$J`+(vHSel+FD=ZW6D%oq$2uer) zxI1!rEZ14@obBM!Vz|9_L#yK@X@Ovl+F!dQ<^YAVoo##a5?xxR9J8Kb!>+VwoA28C zYn~>8TQ$?~Znkd~42&+izWYV8jo{XWDRaBmGYbaZ^|~J3_3%?sRrou8`C|6w64hCm wA?&HL;=B!=BFBUB|Ane?DE^G9y8eS(`VIG+=d%wk1ZG_ZPgg&ebxsLQ0O|dq&Hw-a literal 0 HcmV?d00001 diff --git a/frontend/public/plugin-logos/appointment_reminder.svg b/frontend/public/plugin-logos/appointment_reminder.svg new file mode 100644 index 0000000..1f0ffa8 --- /dev/null +++ b/frontend/public/plugin-logos/appointment_reminder.svg @@ -0,0 +1 @@ + diff --git a/frontend/public/plugin-logos/backup_database.svg b/frontend/public/plugin-logos/backup_database.svg new file mode 100644 index 0000000..d9a4d50 --- /dev/null +++ b/frontend/public/plugin-logos/backup_database.svg @@ -0,0 +1 @@ + diff --git a/frontend/public/plugin-logos/birthday-greetings.png b/frontend/public/plugin-logos/birthday-greetings.png new file mode 100644 index 0000000000000000000000000000000000000000..648d415f2db146e29117764344767d443df19383 GIT binary patch literal 840 zcmeAS@N?(olHy`uVBq!ia0vp^6F``Q4M;wBd$farftlaa#WAE}&f7b0eWe{m+AeN& zWq0(F)?}9AG<8&7*t12D)2hRzPSP@M@4b)K_xf)Z|5M_h_U`fSZrwev)joXt@W=4z z`-iK4UtfM#_x%2o51S&_-Es`I^|5#zsJ-Nez{NH3LYM7+9(|PQY;=6}DPh0eUfTmd zvI+U$_Ie&IGNEGI!jyB1nT5`8>zVs~MZ=NJzRAz(3>7}+++O;VpG`$>SKf~1&5WJx z${Z;ub{e8dw=K0&*cI(#6urhfOoXi&g zdyi?h)q+Wm^Iu8Vy!{+?;f-y8(VKn8uiLE`^<@jmj%Y8=nty$g8gr7>S=Nx>q5Hmk zes(R!!N=PH=%OV%8?yG_-vkv}eX=2I?wn6xwOsSG^4=s~ z7Z;GDa1dfY6JN>1$L*Y^e=|l(nmqeBI!-A4*MIZ(W9qx*2RzV%{O0}rc5k2639Xq9 zbx6wn(%9#A9oct);yDUR^BHeCO|kfr#0-kE|4c=a$vuz#_T?xD8a~;=KixoSFHjyB z`;XqR2?-uQ_tl_+Q&=$8{LaCn)e3^Sz{JwOt5fWvS?|o>FZLgD*`k~LJoZcao(@pz zI{Kcub4Bjb&%H?%)fN9dgTe~DWM4fatn9K literal 0 HcmV?d00001 diff --git a/frontend/public/plugin-logos/cleanup_old_events.svg b/frontend/public/plugin-logos/cleanup_old_events.svg new file mode 100644 index 0000000..5c1238c --- /dev/null +++ b/frontend/public/plugin-logos/cleanup_old_events.svg @@ -0,0 +1 @@ + diff --git a/frontend/public/plugin-logos/daily-appointment-summary.png b/frontend/public/plugin-logos/daily-appointment-summary.png new file mode 100644 index 0000000000000000000000000000000000000000..55f545adb33d94a780644d5f69c3417193196f91 GIT binary patch literal 1122 zcmeAS@N?(olHy`uVBq!ia0vp^6F``Q4M;wBd$farfko5P#WAE}&fB}bMYr8VTpxOe zt=iOZW>IX4;5CC=-p?W@oAQWHjCI+(Mp1G{RGTA1&IhKy-_MzcigGd?Uw?6fW_-wj z_woOJu0Qho!M)#apB6oPJ^#}qmdooFH(Px<%CTJDy)|V&$Hgz_J72!qWB9S>;Lbk( zsqPk!Z&~-2`71nQYrtgF+o#I;PUb{LC<$bI4pJ3+Ly@bEMB*&mLYpa#P_H@ycuOoTPrqY zWqqwNv2I-QUS0Q-NtWrHCBImzs{d9VKx#Igu9$Ce!?GK6B9v5Kr%+3Akesj@t3AF`F%3K7Oox4)N==oKtORWFO zOQ0bq*VY{=n53wF;mXa+8bvli4Gl{^D|N~Bi>N5Pys@@!3eb83M$h9pUzKw&7PoL| zRGKL*JGb}M{(DQGdEOUh`KTjZCOu!deD(a#Qh}{8hn~&rRj9HNVywL-&HS-OxQcUj zb@ksB+j9PXdpiAa{AZ2&`~PD9N-~~3^S*mopewh!=kXmPLf$vJDs=)M&y2E)W4bkA zk>b4f&yM>#h<=gsH?pl`YHDrZdR_ZVO3-huMu>j3VSh=qyF;d^#T9{+nBH@np%>C`02t#uo zOCOz_(c<8+VMB{)m-U`p0M>L!=$ghnm*k5Y|JRt; z@`03Sy{P5#?VsPE_0n|F&qCn<*S)S=O7^(^zn-=xyYl*0Q7guShlJQpfBpNdXl1Ry z3vpo%h{-#>guXjg90TSjpevsmD=ZWE=CK!&c5KPQk+kRHHr$ZsP{KE&ij*g`2lE0ht7IkVYoAY|!UT)<@5k>32ugkR& zJi6hz)%!X=7nkJQ)wOFSez+aCm?z6~ZF0u4ixP8QS7b2wEUA diff --git a/frontend/public/plugin-logos/generate_all_logos_gemini.py b/frontend/public/plugin-logos/generate_all_logos_gemini.py new file mode 100644 index 0000000..38d41d5 --- /dev/null +++ b/frontend/public/plugin-logos/generate_all_logos_gemini.py @@ -0,0 +1,132 @@ +#!/usr/bin/env python3 +from google import genai +from google.genai import types +from PIL import Image +from io import BytesIO +import os +import time + +# Setup Client +client = genai.Client(api_key="AIzaSyB-nR0nkeftKrd42NrNIDcFCj3yFP8JLtw") + +OUTPUT_DIR = "/home/poduck/Desktop/smoothschedule2/frontend/public/plugin-logos" + +# Plugin configurations with detailed prompts +plugins = [ + { + 'filename': 'daily-appointment-summary.png', + 'prompt': '''Create a modern, minimalist app icon in square format with rounded corners. +Design: Indigo blue gradient background (#4f46e5 to lighter blue). +Icon: Simple white envelope overlaid with a small calendar icon in the corner. +Style: Flat design, clean geometric shapes, professional SaaS aesthetic. +The icon should be crisp and recognizable at 48x48 pixels.''' + }, + { + 'filename': 'no-show-tracker.png', + 'prompt': '''Create a modern, minimalist app icon in square format with rounded corners. +Design: Red gradient background (#dc2626 to darker red). +Icon: Simple white person silhouette with a bold white X symbol overlaid. +Style: Flat design, clean geometric shapes, professional warning aesthetic. +The icon should clearly convey "missed appointment" at small sizes.''' + }, + { + 'filename': 'birthday-greetings.png', + 'prompt': '''Create a modern, minimalist app icon in square format with rounded corners. +Design: Pink gradient background (#ec4899 to lighter pink). +Icon: Simple white birthday cake with 3 candles or a white gift box with a bow. +Style: Flat design, clean geometric shapes, cheerful yet professional aesthetic. +The icon should feel celebratory and friendly at small sizes.''' + }, + { + 'filename': 'monthly-revenue-report.png', + 'prompt': '''Create a modern, minimalist app icon in square format with rounded corners. +Design: Green gradient background (#10b981 to darker green). +Icon: Simple white upward trending line chart or ascending bar graph. +Style: Flat design, clean geometric shapes, professional business analytics aesthetic. +The icon should convey growth and success at small sizes.''' + }, + { + 'filename': 'appointment-reminder-24hr.png', + 'prompt': '''Create a modern, minimalist app icon in square format with rounded corners. +Design: Amber/orange gradient background (#f59e0b to darker orange). +Icon: Simple white notification bell with a small red circular alert badge. +Style: Flat design, clean geometric shapes, attention-grabbing yet professional aesthetic. +The icon should clearly indicate alerts and reminders at small sizes.''' + }, + { + 'filename': 'inactive-customer-reengagement.png', + 'prompt': '''Create a modern, minimalist app icon in square format with rounded corners. +Design: Purple gradient background (#8b5cf6 to darker purple). +Icon: Simple white heart symbol with a circular white refresh/return arrow around it. +Style: Flat design, clean geometric shapes, warm and welcoming professional aesthetic. +The icon should convey customer care and returning customers at small sizes.''' + } +] + +def generate_logo(prompt, filename): + """Generate a logo using Gemini 2.5 Flash Image""" + print(f"\nGenerating {filename}...") + + try: + response = client.models.generate_content( + model='gemini-2.5-flash-image', + contents=prompt, + config=types.GenerateContentConfig( + response_modalities=["IMAGE"] + ) + ) + + # Save the Image + if response.candidates[0].content.parts: + for part in response.candidates[0].content.parts: + if part.inline_data: + image_data = part.inline_data.data + image = Image.open(BytesIO(image_data)) + + # Save to output directory + output_path = os.path.join(OUTPUT_DIR, filename) + image.save(output_path) + print(f"✓ Saved: {output_path}") + return True + else: + print(f"✗ Model returned text instead of image: {part.text[:100]}") + return False + else: + print("✗ No content parts in response") + return False + + except Exception as e: + error_msg = str(e) + if "RESOURCE_EXHAUSTED" in error_msg or "quota" in error_msg.lower(): + print(f"✗ Quota exceeded. Please try again tomorrow when quota resets.") + print(f" Error: {error_msg[:200]}...") + else: + print(f"✗ Error: {type(e).__name__}: {error_msg[:200]}") + return False + +def main(): + os.makedirs(OUTPUT_DIR, exist_ok=True) + + print("=" * 70) + print("Generating Plugin Logos with Gemini 2.5 Flash Image") + print("=" * 70) + + success_count = 0 + for i, plugin in enumerate(plugins): + if i > 0: + # Wait 3 seconds between requests to avoid rate limiting + print("\nWaiting 3 seconds before next request...") + time.sleep(3) + + if generate_logo(plugin['prompt'], plugin['filename']): + success_count += 1 + + print("\n" + "=" * 70) + print(f"Generation complete: {success_count}/{len(plugins)} successful") + print("=" * 70) + + if success_count == 0: + print("\nNote: If you hit quota limits, try again tomorrow when the quota resets!") + +if __name__ == "__main__": + main() diff --git a/frontend/public/plugin-logos/generate_daily_summary.py b/frontend/public/plugin-logos/generate_daily_summary.py new file mode 100644 index 0000000..d7872f3 --- /dev/null +++ b/frontend/public/plugin-logos/generate_daily_summary.py @@ -0,0 +1,77 @@ +import os +import sys + +try: + from PIL import Image, ImageDraw +except ImportError: + print("Error: Pillow library not found.") + print("Please install it using: pip install Pillow") + sys.exit(1) + +# Configuration +OUTPUT_PATH = "/home/poduck/Desktop/smoothschedule2/frontend/public/plugin-logos/daily-appointment-summary.png" +SIZE = (144, 144) # Generated at 3x resolution (144px) for high quality on 48px displays +BG_COLOR = "#4f46e5" # Indigo/Blue +ICON_COLOR = "white" + +def create_logo(): + # Create a new image with a transparent background + img = Image.new('RGBA', SIZE, (0, 0, 0, 0)) + draw = ImageDraw.Draw(img) + + # 1. Draw Background (Rounded Square) + radius = 30 + rect_bounds = [0, 0, SIZE[0], SIZE[1]] + draw.rounded_rectangle(rect_bounds, radius=radius, fill=BG_COLOR) + + # 2. Draw Envelope Icon (Email concept) + # Centered, slightly offset up to make room for calendar + env_w = 90 + env_h = 60 + env_x = (SIZE[0] - env_w) // 2 + env_y = (SIZE[1] - env_h) // 2 - 10 + + # Envelope body + draw.rectangle([env_x, env_y, env_x + env_w, env_y + env_h], outline=ICON_COLOR, width=5) + + # Envelope flap (V shape) + draw.line([env_x, env_y, env_x + env_w // 2, env_y + env_h // 2 + 5], fill=ICON_COLOR, width=5) + draw.line([env_x + env_w, env_y, env_x + env_w // 2, env_y + env_h // 2 + 5], fill=ICON_COLOR, width=5) + + # 3. Draw Calendar Badge (Scheduling concept) + # Bottom right corner + cal_size = 50 + cal_x = env_x + env_w - (cal_size // 2) + cal_y = env_y + env_h - (cal_size // 2) + + # Clear background behind calendar for separation + border = 4 + draw.rounded_rectangle( + [cal_x - border, cal_y - border, cal_x + cal_size + border, cal_y + cal_size + border], + radius=10, fill=BG_COLOR + ) + + # Calendar body + draw.rounded_rectangle([cal_x, cal_y, cal_x + cal_size, cal_y + cal_size], radius=8, fill="white") + + # Calendar red header (using a lighter indigo/blue to match theme) + header_h = 14 + draw.rounded_rectangle( + [cal_x, cal_y, cal_x + cal_size, cal_y + header_h], + radius=8, corners=(True, True, False, False), fill="#818cf8" + ) + + # Calendar 'lines' + line_x = cal_x + 10 + line_w = cal_size - 20 + draw.line([line_x, cal_y + 22, line_x + line_w, cal_y + 22], fill=BG_COLOR, width=3) + draw.line([line_x, cal_y + 32, line_x + line_w, cal_y + 32], fill=BG_COLOR, width=3) + draw.line([line_x, cal_y + 42, line_x + line_w * 0.6, cal_y + 42], fill=BG_COLOR, width=3) + + # Save the file + os.makedirs(os.path.dirname(OUTPUT_PATH), exist_ok=True) + img.save(OUTPUT_PATH) + print(f"Success! Logo saved to: {OUTPUT_PATH}") + +if __name__ == "__main__": + create_logo() diff --git a/frontend/public/plugin-logos/generate_plugin_logos.py b/frontend/public/plugin-logos/generate_plugin_logos.py new file mode 100644 index 0000000..6144a82 --- /dev/null +++ b/frontend/public/plugin-logos/generate_plugin_logos.py @@ -0,0 +1,155 @@ +import os +import sys + +try: + from PIL import Image, ImageDraw, ImageFont +except ImportError: + print("Error: Pillow library not found.") + print("Please install it using: pip install Pillow") + sys.exit(1) + +# Configuration +OUTPUT_DIR = "/home/poduck/Desktop/smoothschedule2/frontend/public/plugin-logos" +SIZE = (144, 144) +RADIUS = 30 + +def create_base_logo(bg_color): + """Creates the base image with rounded corners and background color.""" + img = Image.new('RGBA', SIZE, (0, 0, 0, 0)) + draw = ImageDraw.Draw(img) + rect_bounds = [0, 0, SIZE[0], SIZE[1]] + draw.rounded_rectangle(rect_bounds, radius=RADIUS, fill=bg_color) + return img, draw + +def save_logo(img, filename): + """Saves the image to the output directory.""" + path = os.path.join(OUTPUT_DIR, filename) + os.makedirs(os.path.dirname(path), exist_ok=True) + img.save(path) + print(f"Saved: {path}") + +# 1. No-Show Customer Tracker (Red, Person with X) +def create_no_show_logo(): + img, draw = create_base_logo("#dc2626") # Red + + # Person Silhouette + cx, cy = 72, 72 + # Head + draw.ellipse([cx-15, 35, cx+15, 65], fill="white") + # Shoulders/Body + draw.rounded_rectangle([cx-30, 70, cx+30, 130], radius=10, fill="white") + + # Cancel Symbol (Red X on white circle or just overlaid) + # Let's use a thick Red X overlay + x_center_y = 85 + half = 20 + width = 8 + + # Draw X + draw.line([cx - half, x_center_y - half, cx + half, x_center_y + half], fill="#dc2626", width=width) + draw.line([cx + half, x_center_y - half, cx - half, x_center_y + half], fill="#dc2626", width=width) + + save_logo(img, "no-show-tracker.png") + +# 2. Birthday Greeting Campaign (Pink, Gift) +def create_birthday_logo(): + img, draw = create_base_logo("#ec4899") # Pink + + # Gift Box + box_w, box_h = 70, 60 + box_x = (SIZE[0] - box_w) // 2 + box_y = (SIZE[1] - box_h) // 2 + 10 + + # Box body + draw.rectangle([box_x, box_y, box_x + box_w, box_y + box_h], fill="white") + + # Ribbons (Lighter Pink) + r_w = 12 + ribbon_color = "#fbcfe8" + # Vertical + draw.rectangle([box_x + (box_w - r_w)//2, box_y, box_x + (box_w + r_w)//2, box_y + box_h], fill=ribbon_color) + # Horizontal + draw.rectangle([box_x, box_y + (box_h - r_w)//2, box_x + box_w, box_y + (box_h + r_w)//2], fill=ribbon_color) + + # Bow + bow_w = 20 + draw.ellipse([72 - bow_w, box_y - 15, 72, box_y], fill="white") + draw.ellipse([72, box_y - 15, 72 + bow_w, box_y], fill="white") + + save_logo(img, "birthday-greetings.png") + +# 3. Monthly Revenue Report (Green, Chart) +def create_revenue_logo(): + img, draw = create_base_logo("#10b981") # Green + + # Bar Chart + margin_bottom = 110 + bar_w = 20 + gap = 10 + start_x = (144 - (3 * bar_w + 2 * gap)) // 2 + + # Bars + heights = [30, 50, 75] + for i, h in enumerate(heights): + x = start_x + i * (bar_w + gap) + draw.rectangle([x, margin_bottom - h, x + bar_w, margin_bottom], fill="white") + + # Trend Arrow (Rising) + arrow_start = (start_x - 5, margin_bottom - 10) + arrow_end = (start_x + 3 * bar_w + 2 * gap + 5, margin_bottom - 90) + + # Simple line for trend + # draw.line([arrow_start, arrow_end], fill="white", width=4) + + save_logo(img, "monthly-revenue-report.png") + +# 4. Appointment Reminder (24hr) (Amber, Bell) +def create_reminder_logo(): + img, draw = create_base_logo("#f59e0b") # Amber + + cx, cy = 72, 70 + r = 30 + + # Bell Dome + draw.chord([cx - r, cy - r, cx + r, cy + r], 180, 0, fill="white") + draw.rectangle([cx - r, cy, cx + r, cy + 20], fill="white") + + # Bell Flare + draw.polygon([(cx - r, cy + 20), (cx + r, cy + 20), (cx + r + 5, cy + 30), (cx - r - 5, cy + 30)], fill="white") + + # Clapper + draw.ellipse([cx - 8, cy + 28, cx + 8, cy + 42], fill="white") + + # Notification Dot + draw.ellipse([cx + 15, cy - 25, cx + 35, cy - 5], fill="#dc2626") + + save_logo(img, "appointment-reminder-24hr.png") + +# 5. Inactive Customer Re-engagement (Purple, Heart) +def create_inactive_logo(): + img, draw = create_base_logo("#8b5cf6") # Purple + + hx, hy = 72, 75 + size = 18 + + # Heart Shape + # Left circle + draw.ellipse([hx - size * 2, hy - size, hx, hy + size], fill="white") + # Right circle + draw.ellipse([hx, hy - size, hx + size * 2, hy + size], fill="white") + # Bottom triangle + draw.polygon([(hx - size * 2 + 1, hy + 4), (hx + size * 2 - 1, hy + 4), (hx, hy + size * 2 + 8)], fill="white") + + # Refresh/Loop Arrow (Subtle arc above) + bbox = [25, 25, 119, 119] + draw.arc(bbox, start=180, end=0, fill="#ddd6fe", width=5) + draw.polygon([(119, 72), (110, 60), (128, 60)], fill="#ddd6fe") + + save_logo(img, "inactive-customer-reengagement.png") + +if __name__ == "__main__": + create_no_show_logo() + create_birthday_logo() + create_revenue_logo() + create_reminder_logo() + create_inactive_logo() diff --git a/frontend/public/plugin-logos/generate_with_2_0_flash.py b/frontend/public/plugin-logos/generate_with_2_0_flash.py new file mode 100644 index 0000000..5571a5f --- /dev/null +++ b/frontend/public/plugin-logos/generate_with_2_0_flash.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python3 +from google import genai +from google.genai import types +from PIL import Image +from io import BytesIO + +# Setup Client +client = genai.Client(api_key="AIzaSyB-nR0nkeftKrd42NrNIDcFCj3yFP8JLtw") + +# Test with a simple prompt +prompt = "Create a simple modern app icon with a blue background and white envelope symbol, square format, flat design, minimalist" + +print(f"Testing gemini-2.0-flash-exp...") +print(f"Prompt: '{prompt}'...") + +try: + response = client.models.generate_content( + model='gemini-2.0-flash-exp', + contents=prompt, + config=types.GenerateContentConfig( + response_modalities=["IMAGE"] + ) + ) + + # Save the Image + if response.candidates[0].content.parts: + for part in response.candidates[0].content.parts: + if part.inline_data: + image_data = part.inline_data.data + image = Image.open(BytesIO(image_data)) + image.save("test_2_0_flash.png") + print("✓ Success! Saved test_2_0_flash.png") + else: + print(f"✗ Model returned text: {part.text[:100]}") + else: + print("✗ No content parts in response") + +except Exception as e: + print(f"✗ Error: {type(e).__name__}: {str(e)[:300]}") diff --git a/frontend/public/plugin-logos/generate_with_gemini.py b/frontend/public/plugin-logos/generate_with_gemini.py new file mode 100644 index 0000000..7e8cbe1 --- /dev/null +++ b/frontend/public/plugin-logos/generate_with_gemini.py @@ -0,0 +1,194 @@ +#!/usr/bin/env python3 +import os +import sys +import requests +import base64 +from pathlib import Path + +# Gemini API configuration +API_KEY = "AIzaSyB-nR0nkeftKrd42NrNIDcFCj3yFP8JLtw" +API_URL = "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash-exp:generateContent" + +OUTPUT_DIR = "/home/poduck/Desktop/smoothschedule2/frontend/public/plugin-logos" + +# Plugin configurations +plugins = [ + { + 'filename': 'daily-appointment-summary.png', + 'prompt': '''Create a modern, professional icon/logo for a plugin called "Daily Appointment Summary Email". + +Design requirements: +- Square icon, 512x512 pixels +- Flat design style with a slight gradient +- Primary color: Indigo/blue (#4f46e5) +- Icon should combine email and calendar/scheduling concepts +- Clean, minimalist design suitable for a SaaS application +- White or light elements on the colored background +- Rounded corners (similar to modern app icons) +- Professional and trustworthy appearance + +The icon represents a plugin that sends daily email summaries of appointments to staff members.''' + }, + { + 'filename': 'no-show-tracker.png', + 'prompt': '''Create a modern, professional icon/logo for a plugin called "No-Show Customer Tracker". + +Design requirements: +- Square icon, 512x512 pixels +- Flat design style with a slight gradient +- Primary color: Red (#dc2626) +- Icon should represent missed appointments or absent customers +- Could show a person silhouette with an X, slash, or cancel symbol +- Clean, minimalist design suitable for a SaaS application +- White or light elements on the colored background +- Rounded corners (similar to modern app icons) +- Professional appearance + +The icon represents a plugin that tracks customers who miss their appointments.''' + }, + { + 'filename': 'birthday-greetings.png', + 'prompt': '''Create a modern, professional icon/logo for a plugin called "Birthday Greeting Campaign". + +Design requirements: +- Square icon, 512x512 pixels +- Flat design style with a slight gradient +- Primary color: Pink (#ec4899) +- Icon should represent birthdays and celebrations +- Could show a birthday cake, gift, party hat, or balloon +- Clean, minimalist design suitable for a SaaS application +- White or light elements on the colored background +- Rounded corners (similar to modern app icons) +- Friendly and celebratory appearance + +The icon represents a plugin that sends birthday emails with special offers to customers.''' + }, + { + 'filename': 'monthly-revenue-report.png', + 'prompt': '''Create a modern, professional icon/logo for a plugin called "Monthly Revenue Report". + +Design requirements: +- Square icon, 512x512 pixels +- Flat design style with a slight gradient +- Primary color: Green (#10b981) +- Icon should represent business growth, analytics, and financial reporting +- Could show an upward trending chart, graph, or money symbol +- Clean, minimalist design suitable for a SaaS application +- White or light elements on the colored background +- Rounded corners (similar to modern app icons) +- Professional and successful appearance + +The icon represents a plugin that generates comprehensive monthly business statistics and revenue reports.''' + }, + { + 'filename': 'appointment-reminder-24hr.png', + 'prompt': '''Create a modern, professional icon/logo for a plugin called "Appointment Reminder (24hr)". + +Design requirements: +- Square icon, 512x512 pixels +- Flat design style with a slight gradient +- Primary color: Amber/Orange (#f59e0b) +- Icon should represent notifications, alerts, and reminders +- Could show a bell with a notification badge, clock, or alarm +- Clean, minimalist design suitable for a SaaS application +- White or light elements on the colored background +- Rounded corners (similar to modern app icons) +- Attention-grabbing but professional appearance + +The icon represents a plugin that sends reminder emails to customers 24 hours before their appointments.''' + }, + { + 'filename': 'inactive-customer-reengagement.png', + 'prompt': '''Create a modern, professional icon/logo for a plugin called "Inactive Customer Re-engagement". + +Design requirements: +- Square icon, 512x512 pixels +- Flat design style with a slight gradient +- Primary color: Purple (#8b5cf6) +- Icon should represent customer retention, returning customers, or re-engagement +- Could show a heart, person with return arrow, refresh symbol, or comeback concept +- Clean, minimalist design suitable for a SaaS application +- White or light elements on the colored background +- Rounded corners (similar to modern app icons) +- Warm and welcoming appearance + +The icon represents a plugin that wins back customers who haven't booked appointments recently.''' + } +] + +def generate_image(prompt, filename): + """Generate an image using Gemini API""" + print(f"\nGenerating {filename}...") + + headers = { + "Content-Type": "application/json" + } + + payload = { + "contents": [{ + "parts": [{ + "text": prompt + }] + }], + "generationConfig": { + "temperature": 1, + "topK": 40, + "topP": 0.95, + "maxOutputTokens": 8192, + } + } + + try: + response = requests.post( + f"{API_URL}?key={API_KEY}", + headers=headers, + json=payload, + timeout=120 + ) + + if response.status_code == 200: + result = response.json() + + # Check if there's an image in the response + if 'candidates' in result and len(result['candidates']) > 0: + candidate = result['candidates'][0] + if 'content' in candidate and 'parts' in candidate['content']: + for part in candidate['content']['parts']: + if 'inlineData' in part: + # Extract and save the image + image_data = base64.b64decode(part['inlineData']['data']) + output_path = os.path.join(OUTPUT_DIR, filename) + with open(output_path, 'wb') as f: + f.write(image_data) + print(f"✓ Saved: {output_path}") + return True + elif 'text' in part: + print(f"Response: {part['text'][:200]}...") + + print(f"✗ No image generated. Response: {result}") + return False + else: + print(f"✗ API Error ({response.status_code}): {response.text}") + return False + + except Exception as e: + print(f"✗ Error: {str(e)}") + return False + +def main(): + # Create output directory + os.makedirs(OUTPUT_DIR, exist_ok=True) + + print("Starting logo generation with Gemini API...") + print("=" * 60) + + success_count = 0 + for plugin in plugins: + if generate_image(plugin['prompt'], plugin['filename']): + success_count += 1 + + print("\n" + "=" * 60) + print(f"Generation complete: {success_count}/{len(plugins)} successful") + +if __name__ == "__main__": + main() diff --git a/frontend/public/plugin-logos/generate_with_gemini_sdk.py b/frontend/public/plugin-logos/generate_with_gemini_sdk.py new file mode 100644 index 0000000..742694a --- /dev/null +++ b/frontend/public/plugin-logos/generate_with_gemini_sdk.py @@ -0,0 +1,112 @@ +#!/usr/bin/env python3 +import google.generativeai as genai +import os +from PIL import Image +import io + +# Configure API Key +genai.configure(api_key="AIzaSyB-nR0nkeftKrd42NrNIDcFCj3yFP8JLtw") + +OUTPUT_DIR = "/home/poduck/Desktop/smoothschedule2/frontend/public/plugin-logos" + +# Plugin configurations +plugins = [ + { + 'filename': 'daily-appointment-summary.png', + 'prompt': '''Create a modern, minimalist app icon in a square format with rounded corners. +Design: Indigo blue gradient background (#4f46e5). White simple envelope icon combined with a small calendar symbol. +Style: Flat design, clean geometric shapes, professional SaaS application aesthetic. +The icon should be instantly recognizable at 48x48 pixels.''' + }, + { + 'filename': 'no-show-tracker.png', + 'prompt': '''Create a modern, minimalist app icon in a square format with rounded corners. +Design: Red gradient background (#dc2626). White simple person silhouette with a bold X or cancel symbol overlay. +Style: Flat design, clean geometric shapes, professional SaaS application aesthetic. +The icon should clearly convey "missed appointment" at small sizes.''' + }, + { + 'filename': 'birthday-greetings.png', + 'prompt': '''Create a modern, minimalist app icon in a square format with rounded corners. +Design: Pink gradient background (#ec4899). White simple birthday cake with candles or gift box with bow. +Style: Flat design, clean geometric shapes, cheerful yet professional aesthetic. +The icon should feel celebratory and friendly at small sizes.''' + }, + { + 'filename': 'monthly-revenue-report.png', + 'prompt': '''Create a modern, minimalist app icon in a square format with rounded corners. +Design: Green gradient background (#10b981). White simple upward trending line chart or bar graph showing growth. +Style: Flat design, clean geometric shapes, professional business analytics aesthetic. +The icon should convey success and growth at small sizes.''' + }, + { + 'filename': 'appointment-reminder-24hr.png', + 'prompt': '''Create a modern, minimalist app icon in a square format with rounded corners. +Design: Amber/orange gradient background (#f59e0b). White simple notification bell with a small red alert dot or badge. +Style: Flat design, clean geometric shapes, attention-grabbing yet professional aesthetic. +The icon should clearly indicate alerts and reminders at small sizes.''' + }, + { + 'filename': 'inactive-customer-reengagement.png', + 'prompt': '''Create a modern, minimalist app icon in a square format with rounded corners. +Design: Purple gradient background (#8b5cf6). White simple heart symbol with a circular refresh/return arrow around it. +Style: Flat design, clean geometric shapes, warm and welcoming professional aesthetic. +The icon should convey customer care and comeback at small sizes.''' + } +] + +def generate_image(prompt, filename): + """Generate an image using Gemini 2.0 Flash Image Generation""" + print(f"\nGenerating {filename}...") + + try: + # Use the image generation model + model = genai.GenerativeModel('gemini-2.0-flash-exp-image-generation') + + # Generate the image + response = model.generate_content(prompt) + + # Check for generated image + if hasattr(response, 'parts') and response.parts: + for part in response.parts: + if hasattr(part, 'inline_data') and part.inline_data: + # Extract image data + image_data = part.inline_data.data + + # Save the image + output_path = os.path.join(OUTPUT_DIR, filename) + with open(output_path, 'wb') as f: + f.write(image_data) + + print(f"✓ Saved: {output_path} ({len(image_data)} bytes)") + return True + + # If no image data found, check if there's text response + if hasattr(response, 'text'): + print(f"✗ No image generated. Response text: {response.text[:200]}") + else: + print(f"✗ No image generated. Response: {response}") + + return False + + except Exception as e: + print(f"✗ Error: {type(e).__name__}: {str(e)}") + return False + +def main(): + # Create output directory + os.makedirs(OUTPUT_DIR, exist_ok=True) + + print("Starting logo generation with Gemini 2.0 Flash Image Generation...") + print("=" * 60) + + success_count = 0 + for plugin in plugins: + if generate_image(plugin['prompt'], plugin['filename']): + success_count += 1 + + print("\n" + "=" * 60) + print(f"Generation complete: {success_count}/{len(plugins)} successful") + +if __name__ == "__main__": + main() diff --git a/frontend/public/plugin-logos/generate_with_imagen.py b/frontend/public/plugin-logos/generate_with_imagen.py new file mode 100644 index 0000000..9aef91b --- /dev/null +++ b/frontend/public/plugin-logos/generate_with_imagen.py @@ -0,0 +1,146 @@ +#!/usr/bin/env python3 +import os +import sys +import requests +import base64 +import time +from pathlib import Path + +# Imagen API configuration +API_KEY = "AIzaSyB-nR0nkeftKrd42NrNIDcFCj3yFP8JLtw" +MODEL = "imagen-4.0-generate-preview-06-06" +API_URL = f"https://generativelanguage.googleapis.com/v1beta/models/{MODEL}:predict" + +OUTPUT_DIR = "/home/poduck/Desktop/smoothschedule2/frontend/public/plugin-logos" + +# Plugin configurations +plugins = [ + { + 'filename': 'daily-appointment-summary.png', + 'prompt': '''A modern, minimalist app icon for "Daily Appointment Summary Email" plugin. +Square format with rounded corners. Indigo blue gradient background (#4f46e5). +White icon showing a combination of an envelope and a calendar. +Flat design, clean lines, professional SaaS application style. +The icon should be simple and recognizable at small sizes.''' + }, + { + 'filename': 'no-show-tracker.png', + 'prompt': '''A modern, minimalist app icon for "No-Show Customer Tracker" plugin. +Square format with rounded corners. Red gradient background (#dc2626). +White icon showing a person silhouette with an X or cancel symbol overlay. +Flat design, clean lines, professional SaaS application style. +The icon should convey missed appointments clearly at small sizes.''' + }, + { + 'filename': 'birthday-greetings.png', + 'prompt': '''A modern, minimalist app icon for "Birthday Greeting Campaign" plugin. +Square format with rounded corners. Pink gradient background (#ec4899). +White icon showing a birthday cake or gift box with a bow. +Flat design, clean lines, cheerful yet professional style. +The icon should be celebratory and friendly at small sizes.''' + }, + { + 'filename': 'monthly-revenue-report.png', + 'prompt': '''A modern, minimalist app icon for "Monthly Revenue Report" plugin. +Square format with rounded corners. Green gradient background (#10b981). +White icon showing an upward trending chart or bar graph. +Flat design, clean lines, professional business analytics style. +The icon should convey growth and success at small sizes.''' + }, + { + 'filename': 'appointment-reminder-24hr.png', + 'prompt': '''A modern, minimalist app icon for "Appointment Reminder" plugin. +Square format with rounded corners. Amber/orange gradient background (#f59e0b). +White icon showing a notification bell with a small red badge or alert dot. +Flat design, clean lines, attention-grabbing yet professional style. +The icon should convey alerts and reminders clearly at small sizes.''' + }, + { + 'filename': 'inactive-customer-reengagement.png', + 'prompt': '''A modern, minimalist app icon for "Inactive Customer Re-engagement" plugin. +Square format with rounded corners. Purple gradient background (#8b5cf6). +White icon showing a heart with a circular refresh arrow around it. +Flat design, clean lines, warm and welcoming professional style. +The icon should convey customer care and return at small sizes.''' + } +] + +def generate_image(prompt, filename): + """Generate an image using Imagen API""" + print(f"\nGenerating {filename}...") + + headers = { + "Content-Type": "application/json" + } + + payload = { + "instances": [{ + "prompt": prompt + }], + "parameters": { + "sampleCount": 1 + } + } + + try: + response = requests.post( + f"{API_URL}?key={API_KEY}", + headers=headers, + json=payload, + timeout=120 + ) + + if response.status_code == 200: + result = response.json() + + # Check if there's an image in the predictions + if 'predictions' in result and len(result['predictions']) > 0: + prediction = result['predictions'][0] + if 'bytesBase64Encoded' in prediction: + # Extract and save the image + image_data = base64.b64decode(prediction['bytesBase64Encoded']) + output_path = os.path.join(OUTPUT_DIR, filename) + with open(output_path, 'wb') as f: + f.write(image_data) + print(f"✓ Saved: {output_path} ({len(image_data)} bytes)") + return True + elif 'image' in prediction and 'bytesBase64Encoded' in prediction['image']: + image_data = base64.b64decode(prediction['image']['bytesBase64Encoded']) + output_path = os.path.join(OUTPUT_DIR, filename) + with open(output_path, 'wb') as f: + f.write(image_data) + print(f"✓ Saved: {output_path} ({len(image_data)} bytes)") + return True + + print(f"✗ No image generated. Response: {str(result)[:300]}...") + return False + else: + print(f"✗ API Error ({response.status_code}): {response.text[:500]}") + return False + + except Exception as e: + print(f"✗ Error: {str(e)}") + return False + +def main(): + # Create output directory + os.makedirs(OUTPUT_DIR, exist_ok=True) + + print("Starting logo generation with Imagen 4.0 API...") + print("=" * 60) + + success_count = 0 + for i, plugin in enumerate(plugins): + if i > 0: + # Add delay between requests to avoid rate limiting + print("Waiting 2 seconds before next request...") + time.sleep(2) + + if generate_image(plugin['prompt'], plugin['filename']): + success_count += 1 + + print("\n" + "=" * 60) + print(f"Generation complete: {success_count}/{len(plugins)} successful") + +if __name__ == "__main__": + main() diff --git a/frontend/public/plugin-logos/inactive-customer-reengagement.png b/frontend/public/plugin-logos/inactive-customer-reengagement.png new file mode 100644 index 0000000000000000000000000000000000000000..357b4cd825879c8382f3f490f4d04c99d5e33e28 GIT binary patch literal 1351 zcmV-N1-SZ&P)>=RkeR?o=2;#|GwS(NPuEQWsnvDWqaW{r4m^H+ ze(A9v@sazdyGzUMH`%y^;ia}vRW~Ma{7_3I^F}0%e=IWHSavLV=D?EUDf1-Gyp&F( z;t3>q=B6~?B7r1N3`%m&7D)2MA~)|Wfh12%a&kWuNbt%3an6K#cAqm%r77;hN`q_}98)bla4k?tSR;aB zudfDf1a{zM05g0 zmuJC?L{tJvp7(f=h(;jElZIU)8i9mQC501uR3of4me5AkQ}c4bGOfB@M6=3c+3v`CYAsRL?NHo> z<?(np`PrKOdr=K!P7ifkvP03rihr^_-=}(`ib9M15Ml+K@m2 zX!%}@P9T=$VnOxnLnn}^OY67tUEanseE1ZB5q;JK5+)>&Fd>122?-=jNFZTC0tpjz z0(twWyfLIMdBlmfAhX%-m!LMf2&1Fb-eXWK|v`#~!Zz}vLmF!zC4Ab`7h zJzzh6dVv7`Cbq)Eak~%*@X^egcsX7-0s(%SYQ@uWx)KQR)!Z|9J3f~`Fts$9?E`ZQ z-o#mODbT_UB6$^O!QB#;!fOa#>%L|RfdGGDc&m4dX#@iNgyN;n9HtTo;17qrzEY+W z2;dEgwJr-&3IyoMED%sH^r-Rx z!36^B;YXGSkU#*=OAuWiKmxr{Dud*CMFL%sD~06wj|5tAB!}dAj|37XBv68_DWmE0 z5ec-xS{BKZMgr}yNF#Z+xd6c9*XNhfg$SQ$0tpimNSKg7!o&>#+&|r2Mi(OdasPC8 z(F77E-UQMV5+y9YXaWfn5=fYMCyNfeWu+_Ue(BvCAK^DerLmqalr$+`GBkR+O$ z(tJ%a=v(S}I6D|jPog!A=D?C;RRJ`S{7~!UZ!$q75-+t^jel1bGRG9BHeUb$002ov JPDHLkV1k1*U0?tJ literal 0 HcmV?d00001 diff --git a/frontend/public/plugin-logos/monthly-revenue-report.png b/frontend/public/plugin-logos/monthly-revenue-report.png new file mode 100644 index 0000000000000000000000000000000000000000..e440e3a6683a6be7f1d68c3befdc6929b46e2155 GIT binary patch literal 706 zcmeAS@N?(olHy`uVBq!ia0vp^6F``Q4M;wBd$farfvMBe#WAE}&f7bIezzS&90Ju} zF}+xJYF*chb+>${%3jb9;POj)F6H#5xNf4_v}d1=ZJhL`&A;IL?~gx?FG(0Mc8o8LDxcBbDx_xAtEh9id!=Dd55;qd6;g^hA< z%tHLh$98%6FM1g3u#A8Cp0~;Mulfz1Z4h>s=xp$aV=k&OJnpTpCU7!O!be^&ooOOR zhl_(s^>=#_H36Uk78fN2Ax4NGlaipo$?e(k2d@43`?2_=>HhyexBi$FJkvo1B!fjU z0il%5@j5ZL#s923%0i0%b^m5&%=g`wi`9JutOR-u-K{C*pa{7oe(m=F^)(T9o*j0Z6D72n&rqh{^m%2HD!8GYVQ&ErM(M0w4Y7;svp61H; z8BDs^;G=k~r?jwysk6iRc$GzEOJ_%-#odE9s}%%uZ}0p&zeHGfQSPiw?os2u8OgexHLKAa)#_FJ3Xm}&Jycszn=e}z^SwZ dm~?_4%V)|@la+PzoCr*t44$rjF6*2UngBf|9Sr~g literal 0 HcmV?d00001 diff --git a/frontend/public/plugin-logos/no-show-tracker.png b/frontend/public/plugin-logos/no-show-tracker.png new file mode 100644 index 0000000000000000000000000000000000000000..2ab4ee66c2aa3847e1af2ee0b4c33490f7fffe9a GIT binary patch literal 1146 zcmeAS@N?(olHy`uVBq!ia0vp^6F``Q4M;wBd$farfyL3&#WAE}&fB|zMYjw@S|2h` zo@?U#ZT}pDmr=LcO{TEij@UQp&Fatp^L1qnUaZzhwzgd9EIy}S8iJPV85q-nBen3(wQnmnr76BJ ze^I9#gW>NBLH4zCrpu|!dUcO;w*eQc0#_;Xk$2r*zdjw}aargmw3$D*U$VoA5scKaOuFB`Q+e|W4Sb1;3?ne`MZ2c+r{a8M?gYd`8me<%7 zxKj7BEvjHx6aYlde>!&mcFjMjwEbDGd33z_??pCWm8$q0@?A{h^tG34*}Y+DqrRh{ z?2f32&G%Yn2wj-Pzngv0E#p`>mzNAvYfk72ZoTE3<~)V#!ZiNvtc$K?ukq;!V!RdQ z>ryTIRCcRL!=mOu=0Ja&$PY^yLl$!=rOkV}hQaf;Kx4X{wL{SE#m43~lQb1N=ZC~= zM>`1Z*Iw*)twCk};-z6XnI~;iE;z2t_##Y`?L!UQg8HpC;$}q*LGw3%T%5+g;`vtx z)^*XnI7HyVHL@gRa$jOaH8x2-3kYj`JxL>oMB${GKb;njoii;hTIOO*KHJj z-TTlGr8C3v%l?IoTlYxtxXkC}UU7VCi-MHMg~0doBm^%OG<@69c2n|VK*P5^4EIDY z8Z`73d~Z=+l*q8vf+c^a>?MhgHb#}Je}y(YEt~Rhx3am2!pd`*9n)@a{Pt0Ym))al zE=TT8+4KMGlAW0*ZHw)?SY-X|-<-XPjGo6O*DOxRyLh3Y?@C973PYd)L!br@wBqyY zuT{6Il{NCuIEZk8(JLKK$!m_vF%5A+ZQp-a?VUHHQR51)z~&2ur#{&2-?Lozjv%>vV1Pw`RCaTuxQ23N{ zcE|dEN`fwNYh~Xr%3tL8B=Dq#`21507jI1M%ja<3mcJ%*-&$ek7b#ksF9I|3MlFW4 qB{o-ooxh*L#(1f4_1^10xF<57+%WS)L=~`1VeoYIb6Mw<&;$U%9QW}6 literal 0 HcmV?d00001 diff --git a/frontend/public/plugin-logos/send_email.svg b/frontend/public/plugin-logos/send_email.svg new file mode 100644 index 0000000..652110d --- /dev/null +++ b/frontend/public/plugin-logos/send_email.svg @@ -0,0 +1 @@ + diff --git a/frontend/public/plugin-logos/test_gemini_image.py b/frontend/public/plugin-logos/test_gemini_image.py new file mode 100644 index 0000000..399d18a --- /dev/null +++ b/frontend/public/plugin-logos/test_gemini_image.py @@ -0,0 +1,43 @@ +import google.generativeai as genai +import os +from PIL import Image +import io + +# Configure API Key +genai.configure(api_key="AIzaSyB-nR0nkeftKrd42NrNIDcFCj3yFP8JLtw") + +# Try to initialize the model +try: + generation_model = genai.GenerativeModel('gemini-pro-vision') + print("Model initialized successfully") + + # Define prompt + prompt = "A modern app icon with a blue background and white envelope symbol" + print(f"Generating image for: '{prompt}'...") + + # Try to generate + response = generation_model.generate_content(prompt) + + print(f"Response type: {type(response)}") + print(f"Response: {response}") + + if hasattr(response, 'parts') and response.parts: + print(f"Parts: {response.parts}") + if hasattr(response.parts[0], 'inline_data'): + print("Has inline_data!") + image_data = response.parts[0].inline_data.data + image = Image.open(io.BytesIO(image_data)) + image.save("test_output.png") + print("Success! Image saved to test_output.png") + else: + print("No inline_data attribute") + print(f"Part attributes: {dir(response.parts[0])}") + else: + print("No parts in response or response.parts doesn't exist") + if hasattr(response, 'text'): + print(f"Response text: {response.text}") + +except Exception as e: + print(f"Error: {type(e).__name__}: {str(e)}") + import traceback + traceback.print_exc() diff --git a/frontend/public/plugin-logos/test_gemini_native_image.py b/frontend/public/plugin-logos/test_gemini_native_image.py new file mode 100644 index 0000000..5c5eb92 --- /dev/null +++ b/frontend/public/plugin-logos/test_gemini_native_image.py @@ -0,0 +1,41 @@ +from google import genai +from google.genai import types +from PIL import Image +from io import BytesIO +import os + +# Setup Client +client = genai.Client(api_key="AIzaSyB-nR0nkeftKrd42NrNIDcFCj3yFP8JLtw") + +# Define the Prompt +prompt = "Create a simple modern app icon with a blue background and white envelope symbol, square format, flat design, minimalist" + +print(f"Asking Gemini to generate: '{prompt}'...") + +# Call the Gemini Model +try: + response = client.models.generate_content( + model='gemini-2.5-flash-image', + contents=prompt, + config=types.GenerateContentConfig( + response_modalities=["IMAGE"] # Tell Gemini to draw, not talk + ) + ) + + # Save the Image + if response.candidates[0].content.parts: + for part in response.candidates[0].content.parts: + if part.inline_data: + image_data = part.inline_data.data + image = Image.open(BytesIO(image_data)) + image.save("test_gemini_native.png") + print("Success! Saved test_gemini_native.png") + else: + print("Model returned text instead of image:", part.text) + else: + print("No content parts in response") + +except Exception as e: + print(f"Error: {type(e).__name__}: {e}") + import traceback + traceback.print_exc() diff --git a/frontend/public/plugin-logos/test_google_genai.py b/frontend/public/plugin-logos/test_google_genai.py new file mode 100644 index 0000000..298044c --- /dev/null +++ b/frontend/public/plugin-logos/test_google_genai.py @@ -0,0 +1,41 @@ +from google import genai +from google.genai import types +from PIL import Image +from io import BytesIO +import os + +# Initialize the Client +client = genai.Client(api_key="AIzaSyB-nR0nkeftKrd42NrNIDcFCj3yFP8JLtw") + +# Define the prompt +prompt = "A simple modern app icon with a blue background and white envelope symbol, square format, flat design" + +print(f"Generating image for: '{prompt}'...") + +# Call the API +try: + response = client.models.generate_images( + model='gemini-2.5-flash-image', + prompt=prompt, + config=types.GenerateImagesConfig( + number_of_images=1, + aspect_ratio="1:1", + safety_filter_level="BLOCK_LOW_AND_ABOVE", + person_generation="ALLOW_ADULT" + ) + ) + + # Handle the response + for i, generated_image in enumerate(response.generated_images): + # Convert raw bytes to an image + image = Image.open(BytesIO(generated_image.image.image_bytes)) + + # Save to disk + filename = f"test_generated_image_{i}.png" + image.save(filename) + print(f"Success! Saved {filename}") + +except Exception as e: + print(f"Error occurred: {type(e).__name__}: {e}") + import traceback + traceback.print_exc() diff --git a/frontend/public/plugin-logos/webhook.svg b/frontend/public/plugin-logos/webhook.svg new file mode 100644 index 0000000..854198c --- /dev/null +++ b/frontend/public/plugin-logos/webhook.svg @@ -0,0 +1 @@ + diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index c6331dc..607fb5a 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -65,6 +65,7 @@ import HelpPluginDocs from './pages/HelpPluginDocs'; // Import Plugin documentat import PlatformSupport from './pages/PlatformSupport'; // Import Platform Support page (for businesses to contact SmoothSchedule) import PluginMarketplace from './pages/PluginMarketplace'; // Import Plugin Marketplace page import MyPlugins from './pages/MyPlugins'; // Import My Plugins page +import Tasks from './pages/Tasks'; // Import Tasks page for scheduled plugin executions import { Toaster } from 'react-hot-toast'; // Import Toaster for notifications const queryClient = new QueryClient({ @@ -536,6 +537,16 @@ const AppContent: React.FC = () => { ) } /> + + ) : ( + + ) + } + /> } /> {'{{PROMPT:variable|description|default}}'} - Optional field with default value +
+ {'{{PROMPT:variable|description|default|textarea}}'} + - Multi-line text input (for email bodies, long messages) +
+ +
+

+ Field Type Detection +

+

+ The system automatically detects field types from variable names and descriptions: email for email validation, + number for numeric inputs, message/body/content for textareas, url/webhook for URLs. + You can override this by explicitly specifying the type as the 4th parameter. +

@@ -813,9 +827,65 @@ result = {'total': len(appointments), 'by_status': stats}`}

{'{{DATE:friday}}'} - Next Friday
- {/* 4. Validation & Types */} + {/* 4. Insertion Codes */}

- 4. Automatic Validation + 4. Insertion Codes (Dynamic Content) +

+

+ Use insertion codes within your PROMPT template text (like email bodies) to inject dynamic content + at runtime. These are automatically replaced with actual values when the plugin executes. +

+
+

+ Business Information: +

+
+
{'{{BUSINESS_NAME}}'} - Your business name
+
{'{{BUSINESS_EMAIL}}'} - Business contact email
+
{'{{BUSINESS_PHONE}}'} - Business phone number
+
+
+
+

+ Customer & Appointment Data: +

+
+
{'{{CUSTOMER_NAME}}'} - Customer's name (in appointment contexts)
+
{'{{CUSTOMER_EMAIL}}'} - Customer's email address
+
{'{{APPOINTMENT_TIME}}'} - Full appointment date and time
+
{'{{APPOINTMENT_DATE}}'} - Appointment date only
+
{'{{APPOINTMENT_SERVICE}}'} - Service name
+
+
+
+

+ Date & Time: +

+
+
{'{{TODAY}}'} - Today's date (YYYY-MM-DD)
+
{'{{NOW}}'} - Current date and time
+
+
+
+

Example: Email Template with Insertion Codes

+ +{`email_body = '{{PROMPT:email_body|Email Message|Hi {{CUSTOMER_NAME}}, + +This is a reminder about your appointment: + +Date/Time: {{APPOINTMENT_TIME}} +Service: {{APPOINTMENT_SERVICE}} + +If you have questions, contact us at {{BUSINESS_EMAIL}} + +Best regards, +{{BUSINESS_NAME}}||textarea}}'`} + +
+ + {/* 5. Validation & Types */} +

+ 5. Automatic Validation

The system automatically detects field types and validates input: @@ -830,7 +900,8 @@ result = {'total': len(appointments), 'by_status': stats}`}

Pro Tip: Combine all template types for maximum power! Use CONTEXT - for business info, DATE for time logic, and PROMPT only when user input is truly needed. + for business info, DATE for time logic, PROMPT for user configuration, and Insertion Codes + within your email templates for personalized dynamic content.

diff --git a/frontend/src/pages/MyPlugins.tsx b/frontend/src/pages/MyPlugins.tsx index 3d343dc..b6d0998 100644 --- a/frontend/src/pages/MyPlugins.tsx +++ b/frontend/src/pages/MyPlugins.tsx @@ -52,8 +52,10 @@ const MyPlugins: React.FC = () => { const [selectedPlugin, setSelectedPlugin] = useState(null); const [showUninstallModal, setShowUninstallModal] = useState(false); const [showRatingModal, setShowRatingModal] = useState(false); + const [showEditModal, setShowEditModal] = useState(false); const [rating, setRating] = useState(0); const [review, setReview] = useState(''); + const [configValues, setConfigValues] = useState>({}); // Fetch installed plugins const { data: plugins = [], isLoading, error } = useQuery({ @@ -67,6 +69,10 @@ const MyPlugins: React.FC = () => { templateDescription: p.template_description || p.templateDescription, category: p.category, version: p.version, + authorName: p.author_name || p.authorName, + logoUrl: p.logo_url || p.logoUrl, + templateVariables: p.template_variables || p.templateVariables || {}, + configValues: p.config_values || p.configValues || {}, isActive: p.is_active !== undefined ? p.is_active : p.isActive, installedAt: p.installed_at || p.installedAt, hasUpdate: p.has_update !== undefined ? p.has_update : p.hasUpdate || false, @@ -117,6 +123,22 @@ const MyPlugins: React.FC = () => { }, }); + // Edit config mutation + const editConfigMutation = useMutation({ + mutationFn: async ({ pluginId, configValues }: { pluginId: string; configValues: Record }) => { + const { data } = await api.patch(`/api/plugin-installations/${pluginId}/`, { + config_values: configValues, + }); + return data; + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['plugin-installations'] }); + setShowEditModal(false); + setSelectedPlugin(null); + setConfigValues({}); + }, + }); + const handleUninstall = (plugin: PluginInstallation) => { setSelectedPlugin(plugin); setShowUninstallModal(true); @@ -149,6 +171,59 @@ const MyPlugins: React.FC = () => { updateMutation.mutate(plugin.id); }; + const unescapeString = (str: string): string => { + return str + .replace(/\\n/g, '\n') + .replace(/\\r/g, '\r') + .replace(/\\t/g, '\t') + .replace(/\\'/g, "'") + .replace(/\\"/g, '"') + .replace(/\\\\/g, '\\'); + }; + + const handleEdit = (plugin: PluginInstallation) => { + setSelectedPlugin(plugin); + // Convert escape sequences to actual characters for display + const displayValues = { ...plugin.configValues || {} }; + if (plugin.templateVariables) { + Object.entries(plugin.templateVariables).forEach(([key, variable]: [string, any]) => { + if (displayValues[key]) { + displayValues[key] = unescapeString(displayValues[key]); + } + }); + } + setConfigValues(displayValues); + setShowEditModal(true); + }; + + const escapeString = (str: string): string => { + return str + .replace(/\\/g, '\\\\') // Escape backslashes first + .replace(/\n/g, '\\n') + .replace(/\r/g, '\\r') + .replace(/\t/g, '\\t') + .replace(/'/g, "\\'") + .replace(/"/g, '\\"'); + }; + + const submitConfigEdit = () => { + if (selectedPlugin) { + // Convert actual characters back to escape sequences for storage + const storageValues = { ...configValues }; + if (selectedPlugin.templateVariables) { + Object.entries(selectedPlugin.templateVariables).forEach(([key, variable]: [string, any]) => { + if (storageValues[key]) { + storageValues[key] = escapeString(storageValues[key]); + } + }); + } + editConfigMutation.mutate({ + pluginId: selectedPlugin.id, + configValues: storageValues, + }); + } + }; + if (isLoading) { return (
@@ -213,13 +288,25 @@ const MyPlugins: React.FC = () => { {plugins.map((plugin) => (
handleEdit(plugin)} >
{/* Plugin Info */}
+ {plugin.logoUrl && ( + {`${plugin.templateName} { + // Hide image if it fails to load + e.currentTarget.style.display = 'none'; + }} + /> + )}

{plugin.templateName}

@@ -240,6 +327,14 @@ const MyPlugins: React.FC = () => {

+ {plugin.authorName && ( +
+ + {t('plugins.author', 'Author')}: + + {plugin.authorName} +
+ )}
{t('plugins.version', 'Version')}: @@ -284,7 +379,10 @@ const MyPlugins: React.FC = () => {
{plugin.hasUpdate && ( )}
)} + + {/* Edit Config Modal */} + {showEditModal && selectedPlugin && ( +
+
+ {/* Modal Header */} +
+

+ {t('plugins.editConfig', 'Edit Plugin Configuration')} +

+ +
+ + {/* Modal Body */} +
+
+

+ {selectedPlugin.templateName} +

+

+ {selectedPlugin.templateDescription} +

+
+ + {/* Config Fields */} + {selectedPlugin.templateVariables && Object.keys(selectedPlugin.templateVariables).length > 0 ? ( +
+ {Object.entries(selectedPlugin.templateVariables).map(([key, variable]: [string, any]) => ( +
+ + + {variable.type === 'textarea' ? ( +