diff --git a/.gitignore b/.gitignore index ceaeaa38..0ec246cf 100644 --- a/.gitignore +++ b/.gitignore @@ -20,4 +20,5 @@ container.db .next-build .cursor .agents -skills-lock.json \ No newline at end of file +skills-lock.json +.codex/ \ No newline at end of file diff --git a/electron/servers/fastapi/services/image_generation_service.py b/electron/servers/fastapi/services/image_generation_service.py index be951eba..00d4ecef 100644 --- a/electron/servers/fastapi/services/image_generation_service.py +++ b/electron/servers/fastapi/services/image_generation_service.py @@ -111,7 +111,9 @@ class ImageGenerationService: "theme_prompt": prompt.theme_prompt, }, ) - elif image_path.startswith("/app_data/") or image_path.startswith("/static/"): + elif image_path.startswith("/app_data/") or image_path.startswith( + "/static/" + ): return self._to_frontend_url(image_path) raise Exception(f"Image not found at {image_path}") @@ -174,14 +176,20 @@ class ImageGenerationService: response_parts = getattr(response, "parts", None) if not response_parts and getattr(response, "candidates", None): first_candidate = response.candidates[0] if response.candidates else None - content = getattr(first_candidate, "content", None) if first_candidate else None + content = ( + getattr(first_candidate, "content", None) if first_candidate else None + ) response_parts = getattr(content, "parts", None) if content else None image_path = None for part in response_parts or []: if part.inline_data is not None: mime_type = getattr(part.inline_data, "mime_type", "") or "" - ext = mime_type.split("/")[-1] if mime_type.startswith("image/") else "png" + ext = ( + mime_type.split("/")[-1] + if mime_type.startswith("image/") + else "png" + ) image_path = os.path.join(output_directory, f"{uuid.uuid4()}.{ext}") if hasattr(part, "as_image"): part.as_image().save(image_path) @@ -368,28 +376,89 @@ class ImageGenerationService: return image_path def _inject_prompt_into_workflow(self, workflow: dict, prompt: str) -> dict: - """ - Find the prompt node in the workflow and inject the prompt text. - Looks for a node with title 'Input Prompt' (case-insensitive). + def norm(x) -> str: + return str(x or "").strip().lower() - User must rename their prompt node to 'Input Prompt' in ComfyUI. - """ - for node_id, node_data in workflow.items(): - meta = node_data.get("_meta", {}) - title = meta.get("title", "").lower() + def is_link(v) -> bool: + return ( + isinstance(v, (list, tuple)) + and len(v) >= 2 + and isinstance(v[0], str) + and isinstance(v[1], int) + ) - if title == "input prompt": - if "inputs" in node_data and "text" in node_data["inputs"]: - node_data["inputs"]["text"] = prompt - print( - f"Injected prompt into node {node_id}: {meta.get('title', '')}" - ) - return workflow + preferred_keys = ( + "text", "value", "prompt", "string", "content", "instruction", "input", "query" + ) + + # string inputs that are usually NOT prompt text + ignore_keys = { + "filename_prefix", "ckpt_name", "clip_name", "vae_name", "unet_name", + "sampler_name", "scheduler", "type", "device", "model", "lora_name" + } + + visited = set() + + def try_set(node_id: str) -> bool: + node_id = str(node_id) + if node_id in visited: + return False + visited.add(node_id) + + node = workflow.get(node_id) + if not isinstance(node, dict): + return False + + inputs = node.setdefault("inputs", {}) + + # 1) preferred prompt-like keys + for k in preferred_keys: + if k in inputs and isinstance(inputs[k], str): + inputs[k] = prompt + return True + + # 2) fallback: exactly one unambiguous writable string field + string_candidates = [ + k for k, v in inputs.items() + if isinstance(v, str) and k not in ignore_keys + ] + if len(string_candidates) == 1: + inputs[string_candidates[0]] = prompt + return True + + # 3) follow links from ANY input key (node-type agnostic) + for v in inputs.values(): + if is_link(v): + if try_set(v[0]): + return True + elif isinstance(v, list): + for item in v: + if is_link(item) and try_set(item[0]): + return True + + return False + + input_prompt_nodes = [ + node_id + for node_id, node_data in workflow.items() + if norm(node_data.get("_meta", {}).get("title")) == "input prompt" + ] + + if not input_prompt_nodes: + raise ValueError( + "Could not find node with title 'Input Prompt'. Rename your prompt node to 'Input Prompt'." + ) + + for nid in input_prompt_nodes: + if try_set(nid): + return workflow raise ValueError( - "Could not find a node with title 'Input Prompt' in the workflow. Please rename your prompt node to 'Input Prompt' in ComfyUI." + "Found 'Input Prompt', but no writable prompt string field was found directly or through linked nodes." ) + + async def _submit_comfyui_workflow( self, session: aiohttp.ClientSession, comfyui_url: str, workflow: dict ) -> str: diff --git a/electron/servers/nextjs/app/(presentation-generator)/(dashboard)/Components/DashboardSidebar.tsx b/electron/servers/nextjs/app/(presentation-generator)/(dashboard)/Components/DashboardSidebar.tsx index 1e0fbf9f..7cc8cd6c 100644 --- a/electron/servers/nextjs/app/(presentation-generator)/(dashboard)/Components/DashboardSidebar.tsx +++ b/electron/servers/nextjs/app/(presentation-generator)/(dashboard)/Components/DashboardSidebar.tsx @@ -121,10 +121,11 @@ const DashboardSidebar = () => { aria-label={itemLabel} title={itemLabel} > -
Privacy
+Usage Analytics
diff --git a/electron/servers/nextjs/app/(presentation-generator)/presentation/components/PresentationHeader.tsx b/electron/servers/nextjs/app/(presentation-generator)/presentation/components/PresentationHeader.tsx index 431b0978..8d5d86af 100644 --- a/electron/servers/nextjs/app/(presentation-generator)/presentation/components/PresentationHeader.tsx +++ b/electron/servers/nextjs/app/(presentation-generator)/presentation/components/PresentationHeader.tsx @@ -307,7 +307,7 @@ const PresentationHeader = ({ )} > {isEditingTitle ? ( -Slides ({presentationData?.slides?.length})
+Slides ({presentationData?.slides?.length})
Choose a design, set preferences, and generate polished slides.
diff --git a/electron/servers/nextjs/components/CodexConfig.tsx b/electron/servers/nextjs/components/CodexConfig.tsx index 9ae4a3b8..2073e06e 100644 --- a/electron/servers/nextjs/components/CodexConfig.tsx +++ b/electron/servers/nextjs/components/CodexConfig.tsx @@ -24,6 +24,7 @@ import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover"; import { cn } from "@/lib/utils"; import { toast } from "sonner"; import { getApiUrl } from "@/utils/api"; +import { MixpanelEvent, trackEvent } from "@/utils/mixpanel"; interface CodexConfigProps { codexModel: string; @@ -118,6 +119,8 @@ export default function CodexConfig({ const handleSignIn = async () => { try { + + trackEvent(MixpanelEvent.Codex_SignIn_API_Call); onInputChange('codex', 'LLM'); const res = await fetch(getApiUrl("/api/v1/ppt/codex/auth/initiate"), { diff --git a/electron/servers/nextjs/utils/mixpanel.ts b/electron/servers/nextjs/utils/mixpanel.ts index e5db60f5..ee456a6e 100644 --- a/electron/servers/nextjs/utils/mixpanel.ts +++ b/electron/servers/nextjs/utils/mixpanel.ts @@ -11,6 +11,7 @@ export enum MixpanelEvent { Home_SaveConfiguration_API_Call = 'Home Save Configuration API Call', Home_CheckOllamaModelPulled_API_Call = 'Home Check Ollama Model Pulled API Call', Home_DownloadOllamaModel_API_Call = 'Home Download Ollama Model API Call', + Codex_SignIn_API_Call = 'Codex Sign In API Call', Outline_Generate_Presentation_Button_Clicked = 'Outline Generate Presentation Button Clicked', Outline_Select_Template_Button_Clicked = 'Outline Select Template Button Clicked', Outline_Add_Slide_Button_Clicked = 'Outline Add Slide Button Clicked', diff --git a/servers/fastapi/services/image_generation_service.py b/servers/fastapi/services/image_generation_service.py index f9ec1202..29ab85c2 100644 --- a/servers/fastapi/services/image_generation_service.py +++ b/servers/fastapi/services/image_generation_service.py @@ -261,26 +261,85 @@ class ImageGenerationService: return image_path def _inject_prompt_into_workflow(self, workflow: dict, prompt: str) -> dict: - """ - Find the prompt node in the workflow and inject the prompt text. - Looks for a node with title 'Input Prompt' (case-insensitive). + def norm(x) -> str: + return str(x or "").strip().lower() - User must rename their prompt node to 'Input Prompt' in ComfyUI. - """ - for node_id, node_data in workflow.items(): - meta = node_data.get("_meta", {}) - title = meta.get("title", "").lower() + def is_link(v) -> bool: + return ( + isinstance(v, (list, tuple)) + and len(v) >= 2 + and isinstance(v[0], str) + and isinstance(v[1], int) + ) - if title == "input prompt": - if "inputs" in node_data and "text" in node_data["inputs"]: - node_data["inputs"]["text"] = prompt - print( - f"Injected prompt into node {node_id}: {meta.get('title', '')}" - ) - return workflow + preferred_keys = ( + "text", "value", "prompt", "string", "content", "instruction", "input", "query" + ) + + # string inputs that are usually NOT prompt text + ignore_keys = { + "filename_prefix", "ckpt_name", "clip_name", "vae_name", "unet_name", + "sampler_name", "scheduler", "type", "device", "model", "lora_name" + } + + visited = set() + + def try_set(node_id: str) -> bool: + node_id = str(node_id) + if node_id in visited: + return False + visited.add(node_id) + + node = workflow.get(node_id) + if not isinstance(node, dict): + return False + + inputs = node.setdefault("inputs", {}) + + # 1) preferred prompt-like keys + for k in preferred_keys: + if k in inputs and isinstance(inputs[k], str): + inputs[k] = prompt + return True + + # 2) fallback: exactly one unambiguous writable string field + string_candidates = [ + k for k, v in inputs.items() + if isinstance(v, str) and k not in ignore_keys + ] + if len(string_candidates) == 1: + inputs[string_candidates[0]] = prompt + return True + + # 3) follow links from ANY input key (node-type agnostic) + for v in inputs.values(): + if is_link(v): + if try_set(v[0]): + return True + elif isinstance(v, list): + for item in v: + if is_link(item) and try_set(item[0]): + return True + + return False + + input_prompt_nodes = [ + node_id + for node_id, node_data in workflow.items() + if norm(node_data.get("_meta", {}).get("title")) == "input prompt" + ] + + if not input_prompt_nodes: + raise ValueError( + "Could not find node with title 'Input Prompt'. Rename your prompt node to 'Input Prompt'." + ) + + for nid in input_prompt_nodes: + if try_set(nid): + return workflow raise ValueError( - "Could not find a node with title 'Input Prompt' in the workflow. Please rename your prompt node to 'Input Prompt' in ComfyUI." + "Found 'Input Prompt', but no writable prompt string field was found directly or through linked nodes." ) async def _submit_comfyui_workflow( diff --git a/servers/nextjs/package-lock.json b/servers/nextjs/package-lock.json index 1de09b41..d9588d67 100644 --- a/servers/nextjs/package-lock.json +++ b/servers/nextjs/package-lock.json @@ -9,6 +9,7 @@ "version": "0.1.0", "dependencies": { "@babel/standalone": "^7.28.2", + "@babel/traverse": "^7.29.0", "@dnd-kit/core": "^6.3.1", "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2", @@ -117,12 +118,12 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", - "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" }, @@ -130,33 +131,56 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-string-parser": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", - "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.0.tgz", - "integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==", - "dev": true, + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", "license": "MIT", "dependencies": { - "@babel/types": "^7.28.0" + "@babel/types": "^7.29.0" }, "bin": { "parser": "bin/babel-parser.js" @@ -183,15 +207,46 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/types": { - "version": "7.28.2", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz", - "integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==", - "dev": true, + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1" + "@babel/helper-validator-identifier": "^7.28.5" }, "engines": { "node": ">=6.9.0" @@ -6605,6 +6660,18 @@ "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", "license": "MIT" }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", diff --git a/servers/nextjs/package.json b/servers/nextjs/package.json index 28d5797d..108933ac 100644 --- a/servers/nextjs/package.json +++ b/servers/nextjs/package.json @@ -11,6 +11,7 @@ }, "dependencies": { "@babel/standalone": "^7.28.2", + "@babel/traverse": "^7.29.0", "@dnd-kit/core": "^6.3.1", "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2",