From 74a51423492c2b9292068f898cd8a3eaa6b143f8 Mon Sep 17 00:00:00 2001 From: Jeff Emmett Date: Mon, 2 Mar 2026 21:27:11 -0800 Subject: [PATCH] feat: Gemini AI integration + zine generator + fix Ollama network MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add /api/prompt multi-provider endpoint (Gemini Flash/Pro + 4 Ollama models) - Add /api/gemini/image with fallback cascade (gemini-2.0-flash → imagen-3.0) - Add /api/zine/outline, /api/zine/page, /api/zine/regenerate-section - Create folk-zine-gen.ts: 8-page MycroZine generator with editable text and per-section regeneration (text + image independently) - Update folk-prompt.ts: multi-provider model dropdown (Gemini + Ollama) - Update folk-image-gen.ts: add Gemini provider toggle + new styles - Connect rspace to ai-internal Docker network for Ollama access - Fetch GEMINI_API_KEY from Infisical claude-ops/ai (no plaintext secrets) - Update entrypoint.sh: dual Infisical project support (primary + AI) - Install @google/generative-ai, @google/genai SDKs Co-Authored-By: Claude Opus 4.6 --- bun.lock | 162 ++++++++ docker-compose.yml | 8 + entrypoint.sh | 63 +-- lib/folk-image-gen.ts | 21 +- lib/folk-prompt.ts | 18 +- lib/folk-zine-gen.ts | 909 ++++++++++++++++++++++++++++++++++++++++++ lib/index.ts | 1 + package.json | 2 + server/index.ts | 363 +++++++++++++++++ website/canvas.html | 23 +- 10 files changed, 1530 insertions(+), 40 deletions(-) create mode 100644 lib/folk-zine-gen.ts diff --git a/bun.lock b/bun.lock index cc3777a..cafcf7a 100644 --- a/bun.lock +++ b/bun.lock @@ -8,6 +8,8 @@ "@automerge/automerge": "^2.2.8", "@aws-sdk/client-s3": "^3.700.0", "@encryptid/sdk": "file:../encryptid-sdk", + "@google/genai": "^1.43.0", + "@google/generative-ai": "^0.24.1", "@lit/reactive-element": "^2.0.4", "@noble/curves": "^1.8.0", "@noble/hashes": "^1.7.0", @@ -189,6 +191,10 @@ "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], + "@google/genai": ["@google/genai@1.43.0", "", { "dependencies": { "google-auth-library": "^10.3.0", "p-retry": "^4.6.2", "protobufjs": "^7.5.4", "ws": "^8.18.0" }, "peerDependencies": { "@modelcontextprotocol/sdk": "^1.25.2" }, "optionalPeers": ["@modelcontextprotocol/sdk"] }, "sha512-hklCsJNdMlDM1IwcCVcGQFBg2izY0+t5BIGbRsxi2UnKi6AGKL7pqJqmBDNRbw0bYCs4y3NA7TB+fkKfP/Nrdw=="], + + "@google/generative-ai": ["@google/generative-ai@0.24.1", "", {}, "sha512-MqO+MLfM6kjxcKoy0p1wRzG3b4ZZXtPI+z2IE26UogS2Cm/XHO+7gGRBh6gcJsOiIVoH93UwKvW4HdgiOZCy9Q=="], + "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.0.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ=="], "@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.0.4" }, "os": "darwin", "cpu": "x64" }, "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q=="], @@ -227,6 +233,8 @@ "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.33.5", "", { "os": "win32", "cpu": "x64" }, "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg=="], + "@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="], + "@lit-labs/ssr-dom-shim": ["@lit-labs/ssr-dom-shim@1.5.0", "", {}, "sha512-HLomZXMmrCFHSRKESF5vklAKsDY7/fsT/ZhqCu3V0UoW/Qbv8wxmO4W9bx4KnCCF2Zak4yuk+AGraK/bPmI4kA=="], "@lit/reactive-element": ["@lit/reactive-element@2.1.2", "", { "dependencies": { "@lit-labs/ssr-dom-shim": "^1.5.0" } }, "sha512-pbCDiVMnne1lYUIaYNN5wrwQXDtHaYtg7YEFPeW+hws6U47WeFvISGUWekPGKWOP1ygrs0ef0o1VJMk1exos5A=="], @@ -239,6 +247,28 @@ "@pinojs/redact": ["@pinojs/redact@0.4.0", "", {}, "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg=="], + "@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="], + + "@protobufjs/aspromise": ["@protobufjs/aspromise@1.1.2", "", {}, "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="], + + "@protobufjs/base64": ["@protobufjs/base64@1.1.2", "", {}, "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="], + + "@protobufjs/codegen": ["@protobufjs/codegen@2.0.4", "", {}, "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg=="], + + "@protobufjs/eventemitter": ["@protobufjs/eventemitter@1.1.0", "", {}, "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q=="], + + "@protobufjs/fetch": ["@protobufjs/fetch@1.1.0", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.1", "@protobufjs/inquire": "^1.1.0" } }, "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ=="], + + "@protobufjs/float": ["@protobufjs/float@1.0.2", "", {}, "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ=="], + + "@protobufjs/inquire": ["@protobufjs/inquire@1.1.0", "", {}, "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q=="], + + "@protobufjs/path": ["@protobufjs/path@1.1.2", "", {}, "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA=="], + + "@protobufjs/pool": ["@protobufjs/pool@1.1.0", "", {}, "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw=="], + + "@protobufjs/utf8": ["@protobufjs/utf8@1.1.0", "", {}, "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="], + "@remirror/core-constants": ["@remirror/core-constants@3.0.0", "", {}, "sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg=="], "@rollup/plugin-virtual": ["@rollup/plugin-virtual@3.0.2", "", { "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-10monEYsBp3scM4/ND4LNH5Rxvh3e/cVeL3jWTgZ2SrQ+BmUoQcopVQvnaMcOnykb1VkxUFuDAN+0FnpTFRy2A=="], @@ -515,6 +545,8 @@ "@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="], + "@types/retry": ["@types/retry@0.12.0", "", {}, "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA=="], + "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], "@x402/core": ["@x402/core@2.5.0", "", { "dependencies": { "zod": "^3.24.2" } }, "sha512-nUr8HW8WhkU1DvrpUfsRvALy5NF8UWKoFezZOtX61mohxp2lWZpJ2GnvscxDM8nmBAbtIollmksd5z5pj8InXw=="], @@ -531,16 +563,32 @@ "aes-js": ["aes-js@4.0.0-beta.5", "", {}, "sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q=="], + "agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], + "ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="], + "ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], + + "ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], + "apg-js": ["apg-js@4.4.0", "", {}, "sha512-fefmXFknJmtgtNEXfPwZKYkMFX4Fyeyz+fNF6JWp87biGOPslJbCBVU158zvKRZfHBKnJDy8CMM40oLFGkXT8Q=="], "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], "atomic-sleep": ["atomic-sleep@1.0.0", "", {}, "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ=="], + "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + + "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], + + "bignumber.js": ["bignumber.js@9.3.1", "", {}, "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ=="], + "bowser": ["bowser@2.14.1", "", {}, "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg=="], + "brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], + + "buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="], + "bun-types": ["bun-types@1.3.5", "", { "dependencies": { "@types/node": "*" } }, "sha512-inmAYe2PFLs0SUbFOWSVD24sg1jFlMPxOjOSSCYqUgn4Hsc3rDc7dFvfVYjFPNHtov6kgUeulV4SxbuIV/stPw=="], "color": ["color@4.2.3", "", { "dependencies": { "color-convert": "^2.0.1", "color-string": "^1.9.0" } }, "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A=="], @@ -553,8 +601,14 @@ "crelt": ["crelt@1.0.6", "", {}, "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g=="], + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], + "data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="], + + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + "deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="], "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], @@ -571,6 +625,12 @@ "domutils": ["domutils@3.2.2", "", { "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3" } }, "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw=="], + "eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="], + + "ecdsa-sig-formatter": ["ecdsa-sig-formatter@1.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ=="], + + "emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], + "encoding-japanese": ["encoding-japanese@2.2.0", "", {}, "sha512-EuJWwlHPZ1LbADuKTClvHtwbaFn4rOD+dRAbWysqEOXRc2Uui0hJInNJrsdH0c+OhJA4nrCBdSkW4DD5YxAo6A=="], "entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], @@ -583,6 +643,8 @@ "eventemitter3": ["eventemitter3@5.0.1", "", {}, "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="], + "extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="], + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], "fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="], @@ -591,8 +653,24 @@ "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + "fetch-blob": ["fetch-blob@3.2.0", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="], + + "foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="], + + "formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="], + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + "gaxios": ["gaxios@7.1.3", "", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "node-fetch": "^3.3.2", "rimraf": "^5.0.1" } }, "sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ=="], + + "gcp-metadata": ["gcp-metadata@8.1.2", "", { "dependencies": { "gaxios": "^7.0.0", "google-logging-utils": "^1.0.0", "json-bigint": "^1.0.0" } }, "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg=="], + + "glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="], + + "google-auth-library": ["google-auth-library@10.6.1", "", { "dependencies": { "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", "gaxios": "7.1.3", "gcp-metadata": "8.1.2", "google-logging-utils": "1.1.3", "jws": "^4.0.0" } }, "sha512-5awwuLrzNol+pFDmKJd0dKtZ0fPLAtoA5p7YO4ODsDu6ONJUVqbYwvv8y2ZBO5MBNp9TJXigB19710kYpBPdtA=="], + + "google-logging-utils": ["google-logging-utils@1.1.3", "", {}, "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA=="], + "he": ["he@1.2.0", "", { "bin": { "he": "bin/he" } }, "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw=="], "highlight.js": ["highlight.js@11.11.1", "", {}, "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w=="], @@ -603,6 +681,8 @@ "htmlparser2": ["htmlparser2@8.0.2", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.0.1", "entities": "^4.4.0" } }, "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA=="], + "https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], + "iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], "imapflow": ["imapflow@1.2.10", "", { "dependencies": { "@zone-eu/mailsplit": "5.4.8", "encoding-japanese": "2.2.0", "iconv-lite": "0.7.2", "libbase64": "1.3.0", "libmime": "5.3.7", "libqp": "2.1.1", "nodemailer": "8.0.1", "pino": "10.3.1", "socks": "2.8.7" } }, "sha512-tqmk0Gj4YBEnGCjjrUYWIf3Z4tzn4iihUcMkBRbafvHF3LqEiYNCSJAAYYbwERFxlikOJ3zzqtEcoxCUTjMv2Q=="], @@ -611,10 +691,22 @@ "is-arrayish": ["is-arrayish@0.3.4", "", {}, "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA=="], + "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + + "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + "isows": ["isows@1.0.7", "", { "peerDependencies": { "ws": "*" } }, "sha512-I1fSfDCZL5P0v33sVqeTDSpcstAg/N+wF5HS033mogOVIp4B+oHC7oOCsA3axAbBSGTJ8QubbNmnIRN/h8U7hg=="], + "jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="], + + "json-bigint": ["json-bigint@1.0.0", "", { "dependencies": { "bignumber.js": "^9.0.0" } }, "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ=="], + "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + "jwa": ["jwa@2.0.1", "", { "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg=="], + + "jws": ["jws@4.0.1", "", { "dependencies": { "jwa": "^2.0.1", "safe-buffer": "^5.0.1" } }, "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA=="], + "leac": ["leac@0.6.0", "", {}, "sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg=="], "libbase64": ["libbase64@1.3.0", "", {}, "sha512-GgOXd0Eo6phYgh0DJtjQ2tO8dc0IVINtZJeARPeiIJqge+HdsWSuaDTe8ztQ7j/cONByDZ3zeB325AHiv5O0dg=="], @@ -627,16 +719,30 @@ "linkifyjs": ["linkifyjs@4.3.2", "", {}, "sha512-NT1CJtq3hHIreOianA8aSXn6Cw0JzYOuDQbOrSPe7gqFnCpKP++MQe3ODgO3oh2GJFORkAAdqredOa60z63GbA=="], + "long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="], + "lowlight": ["lowlight@3.3.0", "", { "dependencies": { "@types/hast": "^3.0.0", "devlop": "^1.0.0", "highlight.js": "~11.11.0" } }, "sha512-0JNhgFoPvP6U6lE/UdVsSq99tn6DhjjpAj5MxG49ewd2mOBVtwWYIT8ClyABhq198aXXODMU6Ox8DrGy/CpTZQ=="], + "lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], + "mailparser": ["mailparser@3.9.3", "", { "dependencies": { "@zone-eu/mailsplit": "5.4.8", "encoding-japanese": "2.2.0", "he": "1.2.0", "html-to-text": "9.0.5", "iconv-lite": "0.7.2", "libmime": "5.3.7", "linkify-it": "5.0.0", "nodemailer": "7.0.13", "punycode.js": "2.3.1", "tlds": "1.261.0" } }, "sha512-AnB0a3zROum6fLaa52L+/K2SoRJVyFDk78Ea6q1D0ofcZLxWEWDtsS1+OrVqKbV7r5dulKL/AwYQccFGAPpuYQ=="], "markdown-it": ["markdown-it@14.1.1", "", { "dependencies": { "argparse": "^2.0.1", "entities": "^4.4.0", "linkify-it": "^5.0.0", "mdurl": "^2.0.0", "punycode.js": "^2.3.1", "uc.micro": "^2.1.0" }, "bin": { "markdown-it": "bin/markdown-it.mjs" } }, "sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA=="], "mdurl": ["mdurl@2.0.0", "", {}, "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w=="], + "minimatch": ["minimatch@9.0.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="], + + "minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], + "node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="], + + "node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="], + "nodemailer": ["nodemailer@6.10.1", "", {}, "sha512-Z+iLaBGVaSjbIzQ4pX6XV41HrooLsQ10ZWPUehGmuantvzWoDVBnmsdUcOIDM1t+yPor5pDhVlDESgOMEGxhHA=="], "on-exit-leak-free": ["on-exit-leak-free@2.1.2", "", {}, "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA=="], @@ -645,8 +751,16 @@ "ox": ["ox@0.12.4", "", { "dependencies": { "@adraffy/ens-normalize": "^1.11.0", "@noble/ciphers": "^1.3.0", "@noble/curves": "1.9.1", "@noble/hashes": "^1.8.0", "@scure/bip32": "^1.7.0", "@scure/bip39": "^1.6.0", "abitype": "^1.2.3", "eventemitter3": "5.0.1" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-+P+C7QzuwPV8lu79dOwjBKfB2CbnbEXe/hfyyrff1drrO1nOOj3Hc87svHfcW1yneRr3WXaKr6nz11nq+/DF9Q=="], + "p-retry": ["p-retry@4.6.2", "", { "dependencies": { "@types/retry": "0.12.0", "retry": "^0.13.1" } }, "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ=="], + + "package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="], + "parseley": ["parseley@0.12.1", "", { "dependencies": { "leac": "^0.6.0", "peberminta": "^0.9.0" } }, "sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw=="], + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + + "path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], + "peberminta": ["peberminta@0.9.0", "", {}, "sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ=="], "perfect-arrows": ["perfect-arrows@0.3.7", "", {}, "sha512-wEN2gerTPVWl3yqoFEF8OeGbg3aRe2sxNUi9rnyYrCsL4JcI6K2tBDezRtqVrYG0BNtsWLdYiiTrYm+X//8yLQ=="], @@ -705,6 +819,8 @@ "prosemirror-view": ["prosemirror-view@1.41.6", "", { "dependencies": { "prosemirror-model": "^1.20.0", "prosemirror-state": "^1.0.0", "prosemirror-transform": "^1.1.0" } }, "sha512-mxpcDG4hNQa/CPtzxjdlir5bJFDlm0/x5nGBbStB2BWX+XOQ9M8ekEG+ojqB5BcVu2Rc80/jssCMZzSstJuSYg=="], + "protobufjs": ["protobufjs@7.5.4", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg=="], + "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], "punycode.js": ["punycode.js@2.3.1", "", {}, "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA=="], @@ -715,10 +831,16 @@ "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], + "retry": ["retry@0.13.1", "", {}, "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg=="], + + "rimraf": ["rimraf@5.0.10", "", { "dependencies": { "glob": "^10.3.7" }, "bin": { "rimraf": "dist/esm/bin.mjs" } }, "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ=="], + "rollup": ["rollup@4.54.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.54.0", "@rollup/rollup-android-arm64": "4.54.0", "@rollup/rollup-darwin-arm64": "4.54.0", "@rollup/rollup-darwin-x64": "4.54.0", "@rollup/rollup-freebsd-arm64": "4.54.0", "@rollup/rollup-freebsd-x64": "4.54.0", "@rollup/rollup-linux-arm-gnueabihf": "4.54.0", "@rollup/rollup-linux-arm-musleabihf": "4.54.0", "@rollup/rollup-linux-arm64-gnu": "4.54.0", "@rollup/rollup-linux-arm64-musl": "4.54.0", "@rollup/rollup-linux-loong64-gnu": "4.54.0", "@rollup/rollup-linux-ppc64-gnu": "4.54.0", "@rollup/rollup-linux-riscv64-gnu": "4.54.0", "@rollup/rollup-linux-riscv64-musl": "4.54.0", "@rollup/rollup-linux-s390x-gnu": "4.54.0", "@rollup/rollup-linux-x64-gnu": "4.54.0", "@rollup/rollup-linux-x64-musl": "4.54.0", "@rollup/rollup-openharmony-arm64": "4.54.0", "@rollup/rollup-win32-arm64-msvc": "4.54.0", "@rollup/rollup-win32-ia32-msvc": "4.54.0", "@rollup/rollup-win32-x64-gnu": "4.54.0", "@rollup/rollup-win32-x64-msvc": "4.54.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-3nk8Y3a9Ea8szgKhinMlGMhGMw89mqule3KWczxhIzqudyHdCIOHw8WJlj/r329fACjKLEh13ZSk7oE22kyeIw=="], "rope-sequence": ["rope-sequence@1.3.4", "", {}, "sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ=="], + "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], + "safe-stable-stringify": ["safe-stable-stringify@2.5.0", "", {}, "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA=="], "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], @@ -729,6 +851,12 @@ "sharp": ["sharp@0.33.5", "", { "dependencies": { "color": "^4.2.3", "detect-libc": "^2.0.3", "semver": "^7.6.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.33.5", "@img/sharp-darwin-x64": "0.33.5", "@img/sharp-libvips-darwin-arm64": "1.0.4", "@img/sharp-libvips-darwin-x64": "1.0.4", "@img/sharp-libvips-linux-arm": "1.0.5", "@img/sharp-libvips-linux-arm64": "1.0.4", "@img/sharp-libvips-linux-s390x": "1.0.4", "@img/sharp-libvips-linux-x64": "1.0.4", "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", "@img/sharp-libvips-linuxmusl-x64": "1.0.4", "@img/sharp-linux-arm": "0.33.5", "@img/sharp-linux-arm64": "0.33.5", "@img/sharp-linux-s390x": "0.33.5", "@img/sharp-linux-x64": "0.33.5", "@img/sharp-linuxmusl-arm64": "0.33.5", "@img/sharp-linuxmusl-x64": "0.33.5", "@img/sharp-wasm32": "0.33.5", "@img/sharp-win32-ia32": "0.33.5", "@img/sharp-win32-x64": "0.33.5" } }, "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw=="], + "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], + + "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], + + "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + "simple-swizzle": ["simple-swizzle@0.2.4", "", { "dependencies": { "is-arrayish": "^0.3.1" } }, "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw=="], "siwe": ["siwe@2.3.2", "", { "dependencies": { "@spruceid/siwe-parser": "^2.1.2", "@stablelib/random": "^1.0.1", "uri-js": "^4.4.1", "valid-url": "^1.0.9" }, "peerDependencies": { "ethers": "^5.6.8 || ^6.0.8" } }, "sha512-aSf+6+Latyttbj5nMu6GF3doMfv2UYj83hhwZgUF20ky6fTS83uVhkQABdIVnEuS8y1bBdk7p6ltb9SmlhTTlA=="], @@ -743,6 +871,14 @@ "split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="], + "string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], + + "string-width-cjs": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="], + + "strip-ansi-cjs": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "strnum": ["strnum@2.1.2", "", {}, "sha512-l63NF9y/cLROq/yqKXSLtcMeeyOfnSQlfMSlzFt/K73oIaD8DGaQWd7Z34X9GPiKqP5rbSh84Hl4bOlLcjiSrQ=="], "thread-stream": ["thread-stream@4.0.0", "", { "dependencies": { "real-require": "^0.2.0" } }, "sha512-4iMVL6HAINXWf1ZKZjIPcz5wYaOdPhtO8ATvZ+Xqp3BTdaqtAwQkNmKORqcIo5YkQqGXq5cwfswDwMqqQNrpJA=="], @@ -777,6 +913,14 @@ "w3c-keyname": ["w3c-keyname@2.2.8", "", {}, "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ=="], + "web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="], + + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + + "wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="], + + "wrap-ansi-cjs": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + "ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="], "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], @@ -813,6 +957,18 @@ "mailparser/nodemailer": ["nodemailer@7.0.13", "", {}, "sha512-PNDFSJdP+KFgdsG3ZzMXCgquO7I6McjY2vlqILjtJd0hy8wEvtugS9xKRF2NWlPNGxvLCXlTNIae4serI7dinw=="], + "string-width-cjs/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "string-width-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "strip-ansi-cjs/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "wrap-ansi-cjs/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "wrap-ansi-cjs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "wrap-ansi-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "@aws-crypto/sha1-browser/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="], "@aws-crypto/sha256-browser/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="], @@ -821,6 +977,12 @@ "ethers/@types/node/undici-types": ["undici-types@6.19.8", "", {}, "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="], + "string-width-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "wrap-ansi-cjs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "wrap-ansi-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + "@aws-crypto/sha1-browser/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="], "@aws-crypto/sha256-browser/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="], diff --git a/docker-compose.yml b/docker-compose.yml index 7682dc1..ac33c52 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -39,6 +39,11 @@ services: - IMAP_PORT=993 - IMAP_TLS_REJECT_UNAUTHORIZED=false - TWENTY_API_URL=https://rnetwork.online + - OLLAMA_URL=http://ollama:11434 + - INFISICAL_AI_CLIENT_ID=${INFISICAL_AI_CLIENT_ID} + - INFISICAL_AI_CLIENT_SECRET=${INFISICAL_AI_CLIENT_SECRET} + - INFISICAL_AI_PROJECT_SLUG=claude-ops + - INFISICAL_AI_SECRET_PATH=/ai depends_on: rspace-db: condition: service_healthy @@ -150,6 +155,7 @@ services: - rspace-internal - payment-network - rmail-mailcow + - ai-internal rspace-db: image: postgres:16-alpine @@ -262,3 +268,5 @@ networks: name: mailcowdockerized_mailcow-network external: true rspace-internal: + ai-internal: + external: true diff --git a/entrypoint.sh b/entrypoint.sh index 1ab3d8b..8e8ea1c 100644 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -4,6 +4,8 @@ # Required env vars: INFISICAL_CLIENT_ID, INFISICAL_CLIENT_SECRET # Optional: INFISICAL_PROJECT_SLUG (default: rspace), INFISICAL_ENV (default: prod), # INFISICAL_URL (default: http://infisical:8080) +# Optional: INFISICAL_AI_CLIENT_ID, INFISICAL_AI_CLIENT_SECRET — fetches AI keys +# from a second Infisical project (INFISICAL_AI_PROJECT_SLUG, INFISICAL_AI_SECRET_PATH) set -e @@ -11,55 +13,68 @@ INFISICAL_URL="${INFISICAL_URL:-http://infisical:8080}" INFISICAL_ENV="${INFISICAL_ENV:-prod}" INFISICAL_PROJECT_SLUG="${INFISICAL_PROJECT_SLUG:-rspace}" -if [ -z "$INFISICAL_CLIENT_ID" ] || [ -z "$INFISICAL_CLIENT_SECRET" ]; then - echo "[infisical] No credentials set, starting without secret injection" - exec "$@" -fi - -echo "[infisical] Fetching secrets from ${INFISICAL_PROJECT_SLUG}/${INFISICAL_ENV}..." - -# Use Bun's built-in fetch API for HTTP calls and JSON parsing -EXPORTS=$(bun -e " +fetch_secrets() { + # Args: $1=clientId, $2=clientSecret, $3=projectSlug, $4=secretPath, $5=label + bun -e " (async () => { try { - const base = process.env.INFISICAL_URL; + const base = '$INFISICAL_URL'; const auth = await fetch(base + '/api/v1/auth/universal-auth/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - clientId: process.env.INFISICAL_CLIENT_ID, - clientSecret: process.env.INFISICAL_CLIENT_SECRET - }) + body: JSON.stringify({ clientId: '$1', clientSecret: '$2' }) }).then(r => r.json()); - if (!auth.accessToken) { console.error('[infisical] Auth failed'); process.exit(1); } + if (!auth.accessToken) { console.error('[infisical:$5] Auth failed'); process.exit(1); } - const slug = process.env.INFISICAL_PROJECT_SLUG; - const env = process.env.INFISICAL_ENV; - const secrets = await fetch(base + '/api/v3/secrets/raw?workspaceSlug=' + slug + '&environment=' + env + '&secretPath=/&recursive=true', { + const secrets = await fetch(base + '/api/v3/secrets/raw?workspaceSlug=$3&environment=$INFISICAL_ENV&secretPath=$4&recursive=true', { headers: { 'Authorization': 'Bearer ' + auth.accessToken } }).then(r => r.json()); - if (!secrets.secrets) { console.error('[infisical] No secrets returned'); process.exit(1); } + if (!secrets.secrets) { console.error('[infisical:$5] No secrets returned'); process.exit(1); } for (const s of secrets.secrets) { const escaped = s.secretValue.replace(/'/g, \"'\\\\''\" ); console.log('export ' + s.secretKey + \"='\" + escaped + \"'\"); } - } catch (e) { console.error('[infisical] Error:', e.message); process.exit(1); } + } catch (e) { console.error('[infisical:$5] Error:', e.message); process.exit(1); } })(); -" 2>&1) || { - echo "[infisical] WARNING: Failed to fetch secrets, starting with existing env vars" +" 2>&1 +} + +if [ -z "$INFISICAL_CLIENT_ID" ] || [ -z "$INFISICAL_CLIENT_SECRET" ]; then + echo "[infisical] No credentials set, starting without secret injection" + exec "$@" +fi + +# ── Primary project secrets (rspace) ── +echo "[infisical] Fetching secrets from ${INFISICAL_PROJECT_SLUG}/${INFISICAL_ENV}..." +EXPORTS=$(fetch_secrets "$INFISICAL_CLIENT_ID" "$INFISICAL_CLIENT_SECRET" "$INFISICAL_PROJECT_SLUG" "/" "primary") || { + echo "[infisical] WARNING: Failed to fetch primary secrets, starting with existing env vars" exec "$@" } -# Check if we got export statements or error messages if echo "$EXPORTS" | grep -q "^export "; then COUNT=$(echo "$EXPORTS" | grep -c "^export ") eval "$EXPORTS" - echo "[infisical] Injected ${COUNT} secrets" + echo "[infisical] Injected ${COUNT} secrets from ${INFISICAL_PROJECT_SLUG}" else echo "[infisical] WARNING: $EXPORTS" echo "[infisical] Starting with existing env vars" fi +# ── AI project secrets (optional second source) ── +if [ -n "$INFISICAL_AI_CLIENT_ID" ] && [ -n "$INFISICAL_AI_CLIENT_SECRET" ]; then + AI_SLUG="${INFISICAL_AI_PROJECT_SLUG:-claude-ops}" + AI_PATH="${INFISICAL_AI_SECRET_PATH:-/ai}" + echo "[infisical] Fetching AI secrets from ${AI_SLUG}${AI_PATH}..." + AI_EXPORTS=$(fetch_secrets "$INFISICAL_AI_CLIENT_ID" "$INFISICAL_AI_CLIENT_SECRET" "$AI_SLUG" "$AI_PATH" "ai") || true + if echo "$AI_EXPORTS" | grep -q "^export "; then + AI_COUNT=$(echo "$AI_EXPORTS" | grep -c "^export ") + eval "$AI_EXPORTS" + echo "[infisical] Injected ${AI_COUNT} AI secrets from ${AI_SLUG}${AI_PATH}" + else + echo "[infisical] WARNING: Failed to fetch AI secrets: $AI_EXPORTS" + fi +fi + exec "$@" diff --git a/lib/folk-image-gen.ts b/lib/folk-image-gen.ts index a1b8cd7..90939ed 100644 --- a/lib/folk-image-gen.ts +++ b/lib/folk-image-gen.ts @@ -177,7 +177,7 @@ const styles = css` font-size: 13px; } - .style-select { + .style-select, .provider-select { padding: 6px 10px; border: 2px solid #e2e8f0; border-radius: 6px; @@ -218,8 +218,10 @@ export class FolkImageGen extends FolkShape { #images: GeneratedImage[] = []; #isLoading = false; #error: string | null = null; + #provider: "fal" | "gemini" = "fal"; #promptInput: HTMLTextAreaElement | null = null; #styleSelect: HTMLSelectElement | null = null; + #providerSelect: HTMLSelectElement | null = null; #imageArea: HTMLElement | null = null; #generateBtn: HTMLButtonElement | null = null; @@ -245,12 +247,19 @@ export class FolkImageGen extends FolkShape {
+
@@ -273,10 +282,16 @@ export class FolkImageGen extends FolkShape { this.#promptInput = wrapper.querySelector(".prompt-input"); this.#styleSelect = wrapper.querySelector(".style-select"); + this.#providerSelect = wrapper.querySelector(".provider-select"); this.#imageArea = wrapper.querySelector(".image-area"); this.#generateBtn = wrapper.querySelector(".generate-btn"); const closeBtn = wrapper.querySelector(".close-btn") as HTMLButtonElement; + // Provider select + this.#providerSelect?.addEventListener("change", () => { + this.#provider = (this.#providerSelect?.value as "fal" | "gemini") || "fal"; + }); + // Generate button handler this.#generateBtn?.addEventListener("click", (e) => { e.stopPropagation(); @@ -315,8 +330,8 @@ export class FolkImageGen extends FolkShape { this.#renderLoading(); try { - // Call backend API (to be implemented) - const response = await fetch("/api/image-gen", { + const endpoint = this.#provider === "gemini" ? "/api/gemini/image" : "/api/image-gen"; + const response = await fetch(endpoint, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ prompt, style }), diff --git a/lib/folk-prompt.ts b/lib/folk-prompt.ts index 584f7de..b6574a9 100644 --- a/lib/folk-prompt.ts +++ b/lib/folk-prompt.ts @@ -244,7 +244,7 @@ export class FolkPrompt extends FolkShape { #messages: ChatMessage[] = []; #isStreaming = false; #error: string | null = null; - #model = "gemini-1.5-flash"; + #model = "gemini-flash"; #messagesEl: HTMLElement | null = null; #promptInput: HTMLTextAreaElement | null = null; @@ -280,10 +280,16 @@ export class FolkPrompt extends FolkShape {
@@ -323,7 +329,7 @@ export class FolkPrompt extends FolkShape { // Model select this.#modelSelect?.addEventListener("change", () => { - this.#model = this.#modelSelect?.value || "gemini-1.5-flash"; + this.#model = this.#modelSelect?.value || "gemini-flash"; }); // Clear button diff --git a/lib/folk-zine-gen.ts b/lib/folk-zine-gen.ts new file mode 100644 index 0000000..5c433d7 --- /dev/null +++ b/lib/folk-zine-gen.ts @@ -0,0 +1,909 @@ +import { FolkShape } from "./folk-shape"; +import { css, html } from "./tags"; + +const styles = css` + :host { + background: white; + border-radius: 8px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + min-width: 480px; + min-height: 560px; + } + + .header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 8px 12px; + background: linear-gradient(135deg, #f59e0b, #ef4444); + color: white; + border-radius: 8px 8px 0 0; + font-size: 12px; + font-weight: 600; + cursor: move; + } + + .header-title { + display: flex; + align-items: center; + gap: 6px; + } + + .header-actions { + display: flex; + gap: 4px; + } + + .header-actions button { + background: transparent; + border: none; + color: white; + cursor: pointer; + padding: 2px 6px; + border-radius: 4px; + font-size: 14px; + } + + .header-actions button:hover { + background: rgba(255, 255, 255, 0.2); + } + + .content { + display: flex; + flex-direction: column; + height: calc(100% - 36px); + overflow: hidden; + } + + /* ── Phase: Ideation ── */ + .ideation { + padding: 16px; + display: flex; + flex-direction: column; + gap: 12px; + } + + .ideation h3 { + margin: 0; + font-size: 14px; + color: #1e293b; + } + + .topic-input { + width: 100%; + padding: 10px 12px; + border: 2px solid #e2e8f0; + border-radius: 8px; + font-size: 13px; + resize: none; + outline: none; + font-family: inherit; + } + + .topic-input:focus { + border-color: #f59e0b; + } + + .options-row { + display: flex; + gap: 8px; + } + + select { + padding: 6px 10px; + border: 2px solid #e2e8f0; + border-radius: 6px; + font-size: 12px; + background: white; + cursor: pointer; + flex: 1; + } + + .generate-btn { + padding: 10px 20px; + background: linear-gradient(135deg, #f59e0b, #ef4444); + color: white; + border: none; + border-radius: 8px; + font-size: 13px; + font-weight: 600; + cursor: pointer; + transition: opacity 0.2s; + } + + .generate-btn:hover { opacity: 0.9; } + .generate-btn:disabled { opacity: 0.5; cursor: not-allowed; } + + /* ── Phase: Drafts / Feedback ── */ + .pages-view { + flex: 1; + display: flex; + flex-direction: column; + overflow: hidden; + } + + .page-nav { + display: flex; + align-items: center; + justify-content: space-between; + padding: 8px 12px; + border-bottom: 1px solid #e2e8f0; + font-size: 12px; + color: #64748b; + } + + .page-nav button { + padding: 4px 10px; + border: 1px solid #e2e8f0; + border-radius: 4px; + background: white; + cursor: pointer; + font-size: 12px; + } + + .page-nav button:hover { background: #f1f5f9; } + .page-nav button:disabled { opacity: 0.3; cursor: not-allowed; } + + .page-dots { + display: flex; + gap: 4px; + } + + .page-dot { + width: 8px; + height: 8px; + border-radius: 50%; + background: #cbd5e1; + cursor: pointer; + border: none; + padding: 0; + } + + .page-dot.active { background: #f59e0b; } + .page-dot.generated { background: #22c55e; } + .page-dot.generating { background: #f59e0b; animation: pulse 1s infinite; } + + @keyframes pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.4; } + } + + .page-content { + flex: 1; + overflow-y: auto; + padding: 12px; + } + + /* ── Section rendering ── */ + .section { + position: relative; + margin-bottom: 12px; + border: 1px solid #e2e8f0; + border-radius: 8px; + overflow: hidden; + transition: border-color 0.2s; + } + + .section:hover { + border-color: #f59e0b; + } + + .section-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 4px 8px; + background: #f8fafc; + border-bottom: 1px solid #e2e8f0; + font-size: 10px; + color: #94a3b8; + text-transform: uppercase; + letter-spacing: 0.5px; + } + + .section-actions { + display: flex; + gap: 2px; + } + + .section-actions button { + padding: 2px 6px; + border: none; + background: transparent; + cursor: pointer; + font-size: 11px; + border-radius: 3px; + color: #64748b; + } + + .section-actions button:hover { + background: #e2e8f0; + color: #1e293b; + } + + .section-body { + padding: 8px 10px; + } + + .section-text { + width: 100%; + border: none; + outline: none; + font-family: inherit; + font-size: 13px; + line-height: 1.5; + color: #1e293b; + background: transparent; + resize: none; + min-height: 24px; + overflow: hidden; + } + + .section-text.headline { + font-size: 18px; + font-weight: 700; + } + + .section-text.subhead { + font-size: 14px; + font-weight: 500; + color: #475569; + } + + .section-text.pullquote { + font-style: italic; + border-left: 3px solid #f59e0b; + padding-left: 10px; + color: #64748b; + } + + .section-image { + width: 100%; + border-radius: 4px; + display: block; + } + + .section-image-placeholder { + display: flex; + align-items: center; + justify-content: center; + height: 120px; + background: #f1f5f9; + border-radius: 4px; + color: #94a3b8; + font-size: 12px; + } + + /* ── Feedback input ── */ + .feedback-row { + display: flex; + gap: 6px; + padding: 4px 8px 8px; + background: #fffbeb; + border-top: 1px solid #fde68a; + } + + .feedback-input { + flex: 1; + padding: 6px 8px; + border: 1px solid #fde68a; + border-radius: 4px; + font-size: 11px; + outline: none; + font-family: inherit; + } + + .feedback-input:focus { border-color: #f59e0b; } + + .feedback-btn { + padding: 6px 10px; + background: #f59e0b; + color: white; + border: none; + border-radius: 4px; + font-size: 11px; + cursor: pointer; + white-space: nowrap; + } + + .feedback-btn:disabled { opacity: 0.5; cursor: not-allowed; } + + /* ── Bottom bar ── */ + .bottom-bar { + padding: 8px 12px; + border-top: 1px solid #e2e8f0; + display: flex; + gap: 8px; + justify-content: space-between; + align-items: center; + } + + .status { + font-size: 11px; + color: #64748b; + } + + /* ── Loading ── */ + .loading { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 24px; + gap: 12px; + flex: 1; + } + + .spinner { + width: 32px; + height: 32px; + border: 3px solid #e2e8f0; + border-top-color: #f59e0b; + border-radius: 50%; + animation: spin 1s linear infinite; + } + + @keyframes spin { + to { transform: rotate(360deg); } + } + + .error { + color: #ef4444; + padding: 12px; + background: #fef2f2; + border-radius: 6px; + font-size: 13px; + margin: 12px; + } + + .placeholder { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + color: #94a3b8; + text-align: center; + gap: 8px; + padding: 24px; + } + + .placeholder-icon { + font-size: 48px; + opacity: 0.5; + } + + /* ── Progress bar ── */ + .progress-bar { + height: 3px; + background: #e2e8f0; + border-radius: 2px; + overflow: hidden; + } + + .progress-fill { + height: 100%; + background: linear-gradient(90deg, #f59e0b, #ef4444); + transition: width 0.5s ease; + } +`; + +// ── Types ── + +interface ZineSection { + id: string; + type: "text" | "image"; + content?: string; + imagePrompt?: string; + imageUrl?: string; +} + +interface ZinePage { + pageNumber: number; + type: "cover" | "content" | "cta"; + title: string; + sections: ZineSection[]; + hashtags: string[]; +} + +type ZinePhase = "ideation" | "generating" | "editing" | "complete"; + +declare global { + interface HTMLElementTagNameMap { + "folk-zine-gen": FolkZineGen; + } +} + +export class FolkZineGen extends FolkShape { + static override tagName = "folk-zine-gen"; + + static { + const sheet = new CSSStyleSheet(); + const parentRules = Array.from(FolkShape.styles.cssRules) + .map((r) => r.cssText) + .join("\n"); + const childRules = Array.from(styles.cssRules) + .map((r) => r.cssText) + .join("\n"); + sheet.replaceSync(`${parentRules}\n${childRules}`); + this.styles = sheet; + } + + #phase: ZinePhase = "ideation"; + #pages: ZinePage[] = []; + #currentPage = 0; + #isLoading = false; + #error: string | null = null; + #style = "punk-zine"; + #tone = "informative"; + #topic = ""; + #generatingPage = 0; // 0 = not generating + #regeneratingSection: string | null = null; + + // DOM refs + #contentEl: HTMLElement | null = null; + + override createRenderRoot() { + const root = super.createRenderRoot(); + + const wrapper = document.createElement("div"); + wrapper.innerHTML = html` +
+ + 📰 + Zine Generator + +
+ + +
+
+
+ `; + + const slot = root.querySelector("slot"); + const containerDiv = slot?.parentElement as HTMLElement; + if (containerDiv) { + containerDiv.replaceWith(wrapper); + } + + this.#contentEl = wrapper.querySelector(".content"); + const resetBtn = wrapper.querySelector(".reset-btn") as HTMLButtonElement; + const closeBtn = wrapper.querySelector(".close-btn") as HTMLButtonElement; + + resetBtn.addEventListener("click", (e) => { + e.stopPropagation(); + this.#reset(); + }); + + closeBtn.addEventListener("click", (e) => { + e.stopPropagation(); + this.dispatchEvent(new CustomEvent("close")); + }); + + this.#render(); + return root; + } + + #reset() { + this.#phase = "ideation"; + this.#pages = []; + this.#currentPage = 0; + this.#isLoading = false; + this.#error = null; + this.#generatingPage = 0; + this.#regeneratingSection = null; + this.#render(); + } + + #render() { + if (!this.#contentEl) return; + + switch (this.#phase) { + case "ideation": + this.#renderIdeation(); + break; + case "generating": + this.#renderGenerating(); + break; + case "editing": + case "complete": + this.#renderEditing(); + break; + } + } + + // ── Ideation Phase ── + + #renderIdeation() { + if (!this.#contentEl) return; + this.#contentEl.innerHTML = ` +
+

Create a MycroZine

+ +
+ + +
+ + ${this.#error ? `
${this.#escapeHtml(this.#error)}
` : ""} +
+ `; + + const topicInput = this.#contentEl.querySelector(".topic-input") as HTMLTextAreaElement; + const styleSelect = this.#contentEl.querySelector(".style-select") as HTMLSelectElement; + const toneSelect = this.#contentEl.querySelector(".tone-select") as HTMLSelectElement; + const genBtn = this.#contentEl.querySelector(".generate-btn") as HTMLButtonElement; + + topicInput?.addEventListener("input", () => { this.#topic = topicInput.value; }); + topicInput?.addEventListener("pointerdown", (e) => e.stopPropagation()); + styleSelect?.addEventListener("change", () => { this.#style = styleSelect.value; }); + toneSelect?.addEventListener("change", () => { this.#tone = toneSelect.value; }); + genBtn?.addEventListener("click", (e) => { + e.stopPropagation(); + this.#generateOutline(); + }); + } + + async #generateOutline() { + if (!this.#topic.trim() || this.#isLoading) return; + + this.#isLoading = true; + this.#error = null; + this.#render(); + + try { + const res = await fetch("/api/zine/outline", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + topic: this.#topic, + style: this.#style, + tone: this.#tone, + }), + }); + + if (!res.ok) throw new Error(`Outline generation failed: ${res.statusText}`); + const data = await res.json(); + this.#pages = data.pages || []; + this.#currentPage = 0; + this.#phase = "generating"; + this.#isLoading = false; + this.#render(); + + // Start generating page images sequentially + await this.#generateAllPages(); + } catch (e: any) { + this.#error = e.message; + this.#isLoading = false; + this.#render(); + } + } + + // ── Generating Phase ── + + #renderGenerating() { + if (!this.#contentEl) return; + const total = this.#pages.length; + const progress = this.#generatingPage > 0 ? ((this.#generatingPage - 1) / total) * 100 : 0; + + this.#contentEl.innerHTML = ` +
+
+ Generating page ${this.#generatingPage} of ${total}... +
+
+
+ ${this.#pages[this.#generatingPage - 1]?.title || ""} +
+ `; + } + + async #generateAllPages() { + for (let i = 0; i < this.#pages.length; i++) { + this.#generatingPage = i + 1; + this.#render(); + + try { + const res = await fetch("/api/zine/page", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + outline: this.#pages[i], + style: this.#style, + tone: this.#tone, + }), + }); + + if (res.ok) { + const data = await res.json(); + // Update image URLs in sections + if (data.images) { + for (const section of this.#pages[i].sections) { + if (section.type === "image" && data.images[section.id]) { + section.imageUrl = data.images[section.id]; + } + } + } + } + } catch (e: any) { + console.error(`[zine-gen] Page ${i + 1} generation failed:`, e.message); + } + } + + this.#generatingPage = 0; + this.#phase = "editing"; + this.#render(); + } + + // ── Editing Phase (with editable text + per-section regeneration) ── + + #renderEditing() { + if (!this.#contentEl) return; + const page = this.#pages[this.#currentPage]; + if (!page) return; + + const total = this.#pages.length; + const dots = this.#pages.map((p, i) => { + const cls = i === this.#currentPage ? "active" : (p.sections.some(s => s.type === "image" && s.imageUrl) ? "generated" : ""); + return ``; + }).join(""); + + let sectionsHtml = ""; + for (const section of page.sections) { + const isRegenerating = this.#regeneratingSection === section.id; + const sectionLabel = section.id.replace(/([A-Z])/g, " $1").replace(/^./, (s) => s.toUpperCase()); + + sectionsHtml += `
`; + sectionsHtml += `
+ ${this.#escapeHtml(sectionLabel)} +
+ +
+
`; + + if (section.type === "text") { + const textClass = section.id === "headline" ? "headline" : + section.id === "subhead" ? "subhead" : + section.id === "pullquote" ? "pullquote" : ""; + sectionsHtml += `
+ +
`; + } else if (section.type === "image") { + sectionsHtml += `
`; + if (section.imageUrl) { + sectionsHtml += `${this.#escapeHtml(section.imagePrompt || `; + } else { + sectionsHtml += `
${isRegenerating ? '
' : "No image yet — click ↻ to generate"}
`; + } + sectionsHtml += `
`; + } + + // Feedback row for regeneration + sectionsHtml += ``; + + sectionsHtml += `
`; + } + + this.#contentEl.innerHTML = ` + +
+
+ Page ${page.pageNumber} of ${total} — ${this.#escapeHtml(page.title)} + ${page.hashtags?.length ? `${page.hashtags.map(t => this.#escapeHtml(t)).join(" ")}` : ""} +
+ ${sectionsHtml} +
+
+ ${this.#phase === "complete" ? "Complete" : "Editing — click text to edit, ↻ to regenerate sections"} + +
+ ${this.#error ? `
${this.#escapeHtml(this.#error)}
` : ""} + `; + + // ── Wire up event listeners ── + + // Page navigation + this.#contentEl.querySelector(".prev-btn")?.addEventListener("click", (e) => { + e.stopPropagation(); + if (this.#currentPage > 0) { this.#currentPage--; this.#render(); } + }); + this.#contentEl.querySelector(".next-btn")?.addEventListener("click", (e) => { + e.stopPropagation(); + if (this.#currentPage < this.#pages.length - 1) { this.#currentPage++; this.#render(); } + }); + + // Page dots + for (const dot of this.#contentEl.querySelectorAll(".page-dot")) { + dot.addEventListener("click", (e) => { + e.stopPropagation(); + this.#currentPage = parseInt((dot as HTMLElement).dataset.page || "0"); + this.#render(); + }); + } + + // Editable text areas — auto-resize and save + for (const textarea of this.#contentEl.querySelectorAll(".section-text") as NodeListOf) { + // Auto-resize + const autoResize = () => { + textarea.style.height = "auto"; + textarea.style.height = textarea.scrollHeight + "px"; + }; + autoResize(); + textarea.addEventListener("input", () => { + autoResize(); + const sectionId = textarea.dataset.sectionId; + if (sectionId) { + const section = page.sections.find(s => s.id === sectionId); + if (section) section.content = textarea.value; + } + }); + textarea.addEventListener("pointerdown", (e) => e.stopPropagation()); + } + + // Regenerate section buttons — show feedback row + for (const btn of this.#contentEl.querySelectorAll(".regen-section-btn") as NodeListOf) { + btn.addEventListener("click", (e) => { + e.stopPropagation(); + const sectionId = btn.dataset.sectionId!; + const feedbackRow = this.#contentEl!.querySelector(`[data-feedback-for="${sectionId}"]`) as HTMLElement; + if (feedbackRow) { + const isVisible = feedbackRow.style.display !== "none"; + // Hide all feedback rows first + for (const row of this.#contentEl!.querySelectorAll(".feedback-row") as NodeListOf) { + row.style.display = "none"; + } + feedbackRow.style.display = isVisible ? "none" : "flex"; + if (!isVisible) { + const input = feedbackRow.querySelector(".feedback-input") as HTMLInputElement; + input?.focus(); + } + } + }); + } + + // Feedback submit buttons + for (const btn of this.#contentEl.querySelectorAll(".feedback-btn") as NodeListOf) { + btn.addEventListener("click", (e) => { + e.stopPropagation(); + const sectionId = btn.dataset.sectionId!; + const input = this.#contentEl!.querySelector(`.feedback-input[data-section-id="${sectionId}"]`) as HTMLInputElement; + const feedback = input?.value.trim() || ""; + this.#regenerateSection(sectionId, feedback); + }); + } + + // Feedback input enter key + for (const input of this.#contentEl.querySelectorAll(".feedback-input") as NodeListOf) { + input.addEventListener("keydown", (e) => { + if (e.key === "Enter") { + e.preventDefault(); + const sectionId = input.dataset.sectionId!; + this.#regenerateSection(sectionId, input.value.trim()); + } + }); + input.addEventListener("pointerdown", (e) => e.stopPropagation()); + } + + // Download button + this.#contentEl.querySelector(".bottom-bar .generate-btn")?.addEventListener("click", (e) => { + e.stopPropagation(); + this.#downloadZine(); + }); + } + + async #regenerateSection(sectionId: string, feedback: string) { + const page = this.#pages[this.#currentPage]; + const section = page?.sections.find(s => s.id === sectionId); + if (!section) return; + + this.#regeneratingSection = sectionId; + this.#error = null; + this.#render(); + + try { + const res = await fetch("/api/zine/regenerate-section", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + section, + pageTitle: page.title, + style: this.#style, + tone: this.#tone, + feedback, + }), + }); + + if (!res.ok) throw new Error(`Regeneration failed: ${res.statusText}`); + const data = await res.json(); + + if (data.type === "text" && data.content) { + section.content = data.content; + } else if (data.type === "image" && data.url) { + section.imageUrl = data.url; + } + } catch (e: any) { + this.#error = e.message; + } finally { + this.#regeneratingSection = null; + this.#render(); + } + } + + #downloadZine() { + // Create a simple HTML document with all pages for printing + const pagesHtml = this.#pages.map(page => { + const sectionsHtml = page.sections.map(s => { + if (s.type === "text") { + const tag = s.id === "headline" ? "h1" : s.id === "subhead" ? "h2" : s.id === "pullquote" ? "blockquote" : "p"; + return `<${tag}>${this.#escapeHtml(s.content || "")}`; + } + if (s.type === "image" && s.imageUrl) { + return ``; + } + return ""; + }).join("\n"); + + return `
+ ${sectionsHtml} +
${page.hashtags?.join(" ") || ""}
+
`; + }).join("\n"); + + const doc = ` +MycroZine: ${this.#escapeHtml(this.#topic)} + +${pagesHtml}`; + + const blob = new Blob([doc], { type: "text/html" }); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = `mycrozine-${Date.now()}.html`; + a.click(); + URL.revokeObjectURL(url); + } + + #escapeHtml(text: string): string { + const div = document.createElement("div"); + div.textContent = text; + return div.innerHTML; + } + + override toJSON() { + return { + ...super.toJSON(), + type: "folk-zine-gen", + phase: this.#phase, + topic: this.#topic, + style: this.#style, + tone: this.#tone, + pages: this.#pages, + currentPage: this.#currentPage, + }; + } +} diff --git a/lib/index.ts b/lib/index.ts index f56c0c8..2ac5d62 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -37,6 +37,7 @@ export * from "./folk-map"; export * from "./folk-image-gen"; export * from "./folk-video-gen"; export * from "./folk-prompt"; +export * from "./folk-zine-gen"; export * from "./folk-transcription"; // Creative Tools Shapes diff --git a/package.json b/package.json index 3b7b8f3..bb1cbb3 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,8 @@ "@automerge/automerge": "^2.2.8", "@aws-sdk/client-s3": "^3.700.0", "@encryptid/sdk": "file:../encryptid-sdk", + "@google/genai": "^1.43.0", + "@google/generative-ai": "^0.24.1", "@lit/reactive-element": "^2.0.4", "@noble/curves": "^1.8.0", "@noble/hashes": "^1.7.0", diff --git a/server/index.ts b/server/index.ts index 7293711..732fe96 100644 --- a/server/index.ts +++ b/server/index.ts @@ -136,6 +136,21 @@ app.get("/.well-known/webauthn", (c) => { ); }); +// ── Serve generated files from /data/files/generated/ ── +app.get("/data/files/generated/:filename", async (c) => { + const filename = c.req.param("filename"); + if (!filename || filename.includes("..") || filename.includes("/")) { + return c.json({ error: "Invalid filename" }, 400); + } + const dir = resolve(process.env.FILES_DIR || "./data/files", "generated"); + const filePath = resolve(dir, filename); + const file = Bun.file(filePath); + if (!(await file.exists())) return c.notFound(); + const ext = filename.split(".").pop() || ""; + const mimeMap: Record = { png: "image/png", jpg: "image/jpeg", jpeg: "image/jpeg", webp: "image/webp" }; + return new Response(file, { headers: { "Content-Type": mimeMap[ext] || "application/octet-stream", "Cache-Control": "public, max-age=86400" } }); +}); + // ── Space registry API ── app.route("/api/spaces", spaces); @@ -500,6 +515,7 @@ app.post("/api/x402-test", async (c) => { // ── Creative tools API endpoints ── const FAL_KEY = process.env.FAL_KEY || ""; +const GEMINI_API_KEY = process.env.GEMINI_API_KEY || ""; // Image generation via fal.ai Flux Pro app.post("/api/image-gen", async (c) => { @@ -762,6 +778,349 @@ app.post("/api/freecad/:action", async (c) => { } }); +// ── Multi-provider prompt endpoint ── +const GEMINI_MODELS: Record = { + "gemini-flash": "gemini-2.0-flash-exp", + "gemini-pro": "gemini-1.5-pro", +}; +const OLLAMA_MODELS: Record = { + "llama3.2": "llama3.2:3b", + "llama3.1": "llama3.1:8b", + "qwen2.5-coder": "qwen2.5-coder:7b", + "mistral-small": "mistral-small:24b", +}; + +app.post("/api/prompt", async (c) => { + const { messages, model = "gemini-flash" } = await c.req.json(); + if (!messages?.length) return c.json({ error: "messages required" }, 400); + + // Determine provider + if (GEMINI_MODELS[model]) { + if (!GEMINI_API_KEY) return c.json({ error: "GEMINI_API_KEY not configured" }, 503); + + const { GoogleGenerativeAI } = await import("@google/generative-ai"); + const genAI = new GoogleGenerativeAI(GEMINI_API_KEY); + const geminiModel = genAI.getGenerativeModel({ model: GEMINI_MODELS[model] }); + + // Convert chat messages to Gemini contents format + const contents = messages.map((m: { role: string; content: string }) => ({ + role: m.role === "assistant" ? "model" : "user", + parts: [{ text: m.content }], + })); + + try { + const result = await geminiModel.generateContent({ contents }); + const text = result.response.text(); + return c.json({ content: text }); + } catch (e: any) { + console.error("[prompt] Gemini error:", e.message); + return c.json({ error: "Gemini request failed" }, 502); + } + } + + if (OLLAMA_MODELS[model]) { + try { + const ollamaRes = await fetch(`${OLLAMA_URL}/api/chat`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + model: OLLAMA_MODELS[model], + messages: messages.map((m: { role: string; content: string }) => ({ + role: m.role, + content: m.content, + })), + stream: false, + }), + }); + + if (!ollamaRes.ok) { + const err = await ollamaRes.text(); + console.error("[prompt] Ollama error:", err); + return c.json({ error: "Ollama request failed" }, 502); + } + + const data = await ollamaRes.json(); + return c.json({ content: data.message?.content || "" }); + } catch (e: any) { + console.error("[prompt] Ollama unreachable:", e.message); + return c.json({ error: "Ollama unreachable" }, 503); + } + } + + return c.json({ error: `Unknown model: ${model}` }, 400); +}); + +// ── Gemini image generation ── +app.post("/api/gemini/image", async (c) => { + if (!GEMINI_API_KEY) return c.json({ error: "GEMINI_API_KEY not configured" }, 503); + + const { prompt, style, aspect_ratio } = await c.req.json(); + if (!prompt) return c.json({ error: "prompt required" }, 400); + + const styleHints: Record = { + photorealistic: "photorealistic, high detail, natural lighting, ", + illustration: "digital illustration, clean lines, vibrant colors, ", + painting: "oil painting style, brushstrokes visible, painterly, ", + sketch: "pencil sketch, hand-drawn, line art, ", + "punk-zine": "punk zine aesthetic, xerox texture, high contrast, DIY, rough edges, ", + collage: "cut-and-paste collage, mixed media, layered paper textures, ", + vintage: "vintage aesthetic, retro colors, aged paper texture, ", + minimalist: "minimalist design, simple shapes, limited color palette, ", + }; + const enhancedPrompt = (styleHints[style] || "") + prompt; + + const { GoogleGenAI } = await import("@google/genai"); + const ai = new GoogleGenAI({ apiKey: GEMINI_API_KEY }); + + const models = ["gemini-2.0-flash-exp", "imagen-3.0-generate-002"]; + for (const modelName of models) { + try { + if (modelName.startsWith("gemini")) { + const result = await ai.models.generateContent({ + model: modelName, + contents: enhancedPrompt, + config: { responseModalities: ["Text", "Image"] }, + }); + + const parts = result.candidates?.[0]?.content?.parts || []; + for (const part of parts) { + if ((part as any).inlineData) { + const { data: b64, mimeType } = (part as any).inlineData; + const ext = mimeType?.includes("png") ? "png" : "jpg"; + const filename = `gemini-${Date.now()}.${ext}`; + const dir = resolve(process.env.FILES_DIR || "./data/files", "generated"); + await Bun.write(resolve(dir, filename), Buffer.from(b64, "base64")); + const url = `/data/files/generated/${filename}`; + return c.json({ url, image_url: url }); + } + } + } else { + // Imagen API + const result = await ai.models.generateImages({ + model: modelName, + prompt: enhancedPrompt, + config: { + numberOfImages: 1, + aspectRatio: aspect_ratio || "3:4", + }, + }); + const img = (result as any).generatedImages?.[0]; + if (img?.image?.imageBytes) { + const filename = `imagen-${Date.now()}.png`; + const dir = resolve(process.env.FILES_DIR || "./data/files", "generated"); + await Bun.write(resolve(dir, filename), Buffer.from(img.image.imageBytes, "base64")); + const url = `/data/files/generated/${filename}`; + return c.json({ url, image_url: url }); + } + } + } catch (e: any) { + console.error(`[gemini/image] ${modelName} error:`, e.message); + continue; + } + } + + return c.json({ error: "All Gemini image models failed" }, 502); +}); + +// ── Zine generation endpoints ── + +const ZINE_STYLES: Record = { + "punk-zine": "Xerox texture, high contrast B&W, DIY collage, hand-drawn typography", + mycelial: "Organic networks, spore patterns, earth tones, fungal textures, interconnected webs", + minimal: "Clean lines, white space, modern sans-serif, subtle gradients", + collage: "Layered imagery, mixed media textures, vintage photographs", + retro: "1970s aesthetic, earth tones, groovy typography, halftone patterns", + academic: "Diagram-heavy, annotated illustrations, infographic elements", +}; + +const ZINE_TONES: Record = { + rebellious: "Defiant, anti-establishment, punk energy", + regenerative: "Hopeful, nature-inspired, healing, systems-thinking", + playful: "Fun, whimsical, approachable", + informative: "Educational, clear, accessible", + poetic: "Lyrical, metaphorical, evocative", +}; + +app.post("/api/zine/outline", async (c) => { + if (!GEMINI_API_KEY) return c.json({ error: "GEMINI_API_KEY not configured" }, 503); + + const { topic, style = "punk-zine", tone = "informative", pageCount = 8 } = await c.req.json(); + if (!topic) return c.json({ error: "topic required" }, 400); + + const { GoogleGenerativeAI } = await import("@google/generative-ai"); + const genAI = new GoogleGenerativeAI(GEMINI_API_KEY); + const model = genAI.getGenerativeModel({ model: "gemini-1.5-pro" }); + + const systemPrompt = `You are a zine designer creating a ${pageCount}-page MycroZine. +Style: ${ZINE_STYLES[style] || style} +Tone: ${ZINE_TONES[tone] || tone} + +Create a structured outline for a zine about: "${topic}" + +Each page has sections that will be generated independently. Return ONLY valid JSON (no markdown fences): +{ + "pages": [ + { + "pageNumber": 1, + "type": "cover", + "title": "...", + "sections": [ + { "id": "headline", "type": "text", "content": "Main headline text" }, + { "id": "subhead", "type": "text", "content": "Subtitle or tagline" }, + { "id": "hero", "type": "image", "imagePrompt": "Detailed image description for cover art" } + ], + "hashtags": ["#tag1"] + }, + { + "pageNumber": 2, + "type": "content", + "title": "...", + "sections": [ + { "id": "heading", "type": "text", "content": "Section heading" }, + { "id": "body", "type": "text", "content": "Main body text (2-3 paragraphs)" }, + { "id": "pullquote", "type": "text", "content": "A punchy pull quote" }, + { "id": "visual", "type": "image", "imagePrompt": "Detailed description for page illustration" } + ], + "hashtags": ["#tag"] + } + ] +} + +Rules: +- Page 1 is always "cover" type with headline, subhead, hero image +- Pages 2-${pageCount - 1} are "content" with heading, body, pullquote, visual +- Page ${pageCount} is "cta" (call-to-action) with heading, body, action, visual +- Each image prompt should be highly specific, matching the ${style} style +- Text content should match the ${tone} tone +- Make body text substantive (2-3 short paragraphs each)`; + + try { + const result = await model.generateContent(systemPrompt); + const text = result.response.text(); + // Parse JSON (strip markdown fences if present) + const jsonStr = text.replace(/```json\n?/g, "").replace(/```\n?/g, "").trim(); + const outline = JSON.parse(jsonStr); + return c.json(outline); + } catch (e: any) { + console.error("[zine/outline] error:", e.message); + return c.json({ error: "Failed to generate outline" }, 502); + } +}); + +app.post("/api/zine/page", async (c) => { + if (!GEMINI_API_KEY) return c.json({ error: "GEMINI_API_KEY not configured" }, 503); + + const { outline, style = "punk-zine", tone = "informative" } = await c.req.json(); + if (!outline?.pageNumber) return c.json({ error: "outline required with pageNumber" }, 400); + + // Find all image sections and generate them + const imageSections = (outline.sections || []).filter((s: any) => s.type === "image"); + const generatedImages: Record = {}; + + const { GoogleGenAI } = await import("@google/genai"); + const ai = new GoogleGenAI({ apiKey: GEMINI_API_KEY }); + + for (const section of imageSections) { + const styleDesc = ZINE_STYLES[style] || style; + const toneDesc = ZINE_TONES[tone] || tone; + const enhancedPrompt = `${styleDesc} style, ${toneDesc} mood. ${section.imagePrompt}. For a zine page about "${outline.title}".`; + + try { + const result = await ai.models.generateContent({ + model: "gemini-2.0-flash-exp", + contents: enhancedPrompt, + config: { responseModalities: ["Text", "Image"] }, + }); + + const parts = result.candidates?.[0]?.content?.parts || []; + for (const part of parts) { + if ((part as any).inlineData) { + const { data: b64, mimeType } = (part as any).inlineData; + const ext = mimeType?.includes("png") ? "png" : "jpg"; + const filename = `zine-p${outline.pageNumber}-${section.id}-${Date.now()}.${ext}`; + const dir = resolve(process.env.FILES_DIR || "./data/files", "generated"); + await Bun.write(resolve(dir, filename), Buffer.from(b64, "base64")); + generatedImages[section.id] = `/data/files/generated/${filename}`; + break; + } + } + } catch (e: any) { + console.error(`[zine/page] Image gen failed for ${section.id}:`, e.message); + } + } + + return c.json({ + pageNumber: outline.pageNumber, + images: generatedImages, + outline, + }); +}); + +app.post("/api/zine/regenerate-section", async (c) => { + if (!GEMINI_API_KEY) return c.json({ error: "GEMINI_API_KEY not configured" }, 503); + + const { section, pageTitle, style = "punk-zine", tone = "informative", feedback = "" } = await c.req.json(); + if (!section?.id) return c.json({ error: "section with id required" }, 400); + + if (section.type === "image") { + // Regenerate image with feedback + const { GoogleGenAI } = await import("@google/genai"); + const ai = new GoogleGenAI({ apiKey: GEMINI_API_KEY }); + + const styleDesc = ZINE_STYLES[style] || style; + const toneDesc = ZINE_TONES[tone] || tone; + const enhancedPrompt = `${styleDesc} style, ${toneDesc} mood. ${section.imagePrompt}. For a zine page about "${pageTitle}".${feedback ? ` User feedback: ${feedback}` : ""}`; + + try { + const result = await ai.models.generateContent({ + model: "gemini-2.0-flash-exp", + contents: enhancedPrompt, + config: { responseModalities: ["Text", "Image"] }, + }); + + const parts = result.candidates?.[0]?.content?.parts || []; + for (const part of parts) { + if ((part as any).inlineData) { + const { data: b64, mimeType } = (part as any).inlineData; + const ext = mimeType?.includes("png") ? "png" : "jpg"; + const filename = `zine-regen-${section.id}-${Date.now()}.${ext}`; + const dir = resolve(process.env.FILES_DIR || "./data/files", "generated"); + await Bun.write(resolve(dir, filename), Buffer.from(b64, "base64")); + return c.json({ sectionId: section.id, type: "image", url: `/data/files/generated/${filename}` }); + } + } + return c.json({ error: "No image generated" }, 502); + } catch (e: any) { + console.error("[zine/regenerate-section] image error:", e.message); + return c.json({ error: "Image regeneration failed" }, 502); + } + } + + if (section.type === "text") { + // Regenerate text via Gemini + const { GoogleGenerativeAI } = await import("@google/generative-ai"); + const genAI = new GoogleGenerativeAI(GEMINI_API_KEY); + const model = genAI.getGenerativeModel({ model: "gemini-2.0-flash-exp" }); + + const toneDesc = ZINE_TONES[tone] || tone; + const prompt = `Rewrite this zine text section. Section type: "${section.id}". Page title: "${pageTitle}". Tone: ${toneDesc}. +Current text: "${section.content}" +${feedback ? `User feedback: ${feedback}` : "Make it punchier and more engaging."} + +Return ONLY the new text, no quotes or explanation.`; + + try { + const result = await model.generateContent(prompt); + return c.json({ sectionId: section.id, type: "text", content: result.response.text().trim() }); + } catch (e: any) { + console.error("[zine/regenerate-section] text error:", e.message); + return c.json({ error: "Text regeneration failed" }, 502); + } + } + + return c.json({ error: `Unknown section type: ${section.type}` }, 400); +}); + // ── Auto-provision personal space ── app.post("/api/spaces/auto-provision", async (c) => { const token = extractToken(c.req.raw.headers); @@ -1735,6 +2094,10 @@ const server = Bun.serve({ } })(); +// Ensure generated files directory exists +import { mkdirSync } from "node:fs"; +try { mkdirSync(resolve(process.env.FILES_DIR || "./data/files", "generated"), { recursive: true }); } catch {} + ensureDemoCommunity().then(() => console.log("[Demo] Demo community ready")).catch((e) => console.error("[Demo] Failed:", e)); loadAllDocs(syncServer) .then(() => ensureTemplateSeeding()) diff --git a/website/canvas.html b/website/canvas.html index 61e3c8d..f1b27db 100644 --- a/website/canvas.html +++ b/website/canvas.html @@ -1356,6 +1356,7 @@ folk-image-gen, folk-video-gen, folk-prompt, + folk-zine-gen, folk-transcription, folk-video-chat, folk-obs-note, @@ -1376,31 +1377,32 @@ folk-drawfast, folk-freecad, folk-kicad, + folk-zine-gen, folk-rapp { position: absolute; } .connect-mode :is(folk-markdown, folk-wrapper, folk-slide, folk-chat, folk-google-item, folk-piano, folk-embed, folk-calendar, folk-map, - folk-image-gen, folk-video-gen, folk-prompt, folk-transcription, + folk-image-gen, folk-video-gen, folk-prompt, folk-zine-gen, folk-transcription, folk-video-chat, folk-obs-note, folk-workflow-block, folk-itinerary, folk-destination, folk-budget, folk-packing-list, folk-booking, folk-token-mint, folk-token-ledger, folk-choice-vote, folk-choice-rank, folk-choice-spider, folk-social-post, folk-splat, folk-blender, folk-drawfast, - folk-freecad, folk-kicad, folk-rapp) { + folk-freecad, folk-kicad, folk-zine-gen, folk-rapp) { cursor: crosshair; } .connect-mode :is(folk-markdown, folk-wrapper, folk-slide, folk-chat, folk-google-item, folk-piano, folk-embed, folk-calendar, folk-map, - folk-image-gen, folk-video-gen, folk-prompt, folk-transcription, + folk-image-gen, folk-video-gen, folk-prompt, folk-zine-gen, folk-transcription, folk-video-chat, folk-obs-note, folk-workflow-block, folk-itinerary, folk-destination, folk-budget, folk-packing-list, folk-booking, folk-token-mint, folk-token-ledger, folk-choice-vote, folk-choice-rank, folk-choice-spider, folk-social-post, folk-splat, folk-blender, folk-drawfast, - folk-freecad, folk-kicad, folk-rapp):hover { + folk-freecad, folk-kicad, folk-zine-gen, folk-rapp):hover { outline: 2px dashed #3b82f6; outline-offset: 4px; } @@ -1790,6 +1792,7 @@
+ @@ -1967,6 +1970,7 @@ FolkImageGen, FolkVideoGen, FolkPrompt, + FolkZineGen, FolkTranscription, FolkVideoChat, FolkObsNote, @@ -2863,6 +2867,9 @@ shape = document.createElement("folk-prompt"); // Messages history would need to be restored from data.messages break; + case "folk-zine-gen": + shape = document.createElement("folk-zine-gen"); + break; case "folk-transcription": shape = document.createElement("folk-transcription"); // Transcript would need to be restored from data.segments @@ -3101,6 +3108,7 @@ "folk-image-gen": { width: 400, height: 500 }, "folk-video-gen": { width: 450, height: 550 }, "folk-prompt": { width: 450, height: 500 }, + "folk-zine-gen": { width: 500, height: 600 }, "folk-transcription": { width: 400, height: 450 }, "folk-video-chat": { width: 480, height: 400 }, "folk-obs-note": { width: 450, height: 500 }, @@ -3362,6 +3370,7 @@ document.getElementById("new-map").addEventListener("click", () => setPendingTool("folk-map")); document.getElementById("new-image-gen").addEventListener("click", () => setPendingTool("folk-image-gen")); document.getElementById("new-video-gen").addEventListener("click", () => setPendingTool("folk-video-gen")); + document.getElementById("new-zine-gen").addEventListener("click", () => setPendingTool("folk-zine-gen")); document.getElementById("new-prompt").addEventListener("click", () => setPendingTool("folk-prompt")); document.getElementById("new-transcription").addEventListener("click", () => setPendingTool("folk-transcription")); document.getElementById("new-video-chat").addEventListener("click", () => setPendingTool("folk-video-chat")); @@ -3877,7 +3886,7 @@ } canvasContent.addEventListener("contextmenu", (e) => { - const shapeEl = e.target.closest("folk-shape, folk-markdown, folk-wrapper, folk-slide, folk-chat, folk-embed, folk-calendar, folk-map, folk-image-gen, folk-video-gen, folk-prompt, folk-transcription, folk-video-chat, folk-obs-note, folk-workflow-block, folk-itinerary, folk-destination, folk-budget, folk-packing-list, folk-booking, folk-token-mint, folk-token-ledger, folk-choice-vote, folk-choice-rank, folk-choice-spider, folk-social-post, folk-rapp, folk-feed, folk-piano, folk-splat, folk-blender, folk-drawfast, folk-freecad, folk-kicad"); + const shapeEl = e.target.closest("folk-shape, folk-markdown, folk-wrapper, folk-slide, folk-chat, folk-embed, folk-calendar, folk-map, folk-image-gen, folk-video-gen, folk-prompt, folk-zine-gen, folk-transcription, folk-video-chat, folk-obs-note, folk-workflow-block, folk-itinerary, folk-destination, folk-budget, folk-packing-list, folk-booking, folk-token-mint, folk-token-ledger, folk-choice-vote, folk-choice-rank, folk-choice-spider, folk-social-post, folk-rapp, folk-feed, folk-piano, folk-splat, folk-blender, folk-drawfast, folk-freecad, folk-kicad"); if (!shapeEl || !shapeEl.id) return; e.preventDefault(); @@ -4514,7 +4523,7 @@ // Deselect and exit edit modes immediately deselectAll(); selectedShapeId = null; - canvasContent.querySelectorAll("folk-shape, folk-markdown, folk-wrapper, folk-slide, folk-chat, folk-obs-note, folk-rapp, folk-embed, folk-drawfast, folk-prompt, folk-workflow-block").forEach(el => { + canvasContent.querySelectorAll("folk-shape, folk-markdown, folk-wrapper, folk-slide, folk-chat, folk-obs-note, folk-rapp, folk-embed, folk-drawfast, folk-prompt, folk-zine-gen, folk-workflow-block").forEach(el => { if (el.exitEditMode) el.exitEditMode(); }); @@ -4724,7 +4733,7 @@ function sortFeedShapes(key) { const shapes = [...canvasContent.querySelectorAll( - 'folk-shape, folk-markdown, folk-wrapper, folk-slide, folk-chat, folk-obs-note, folk-rapp, folk-embed, folk-drawfast, folk-prompt, folk-workflow-block, folk-choice-vote, folk-choice-rank, folk-choice-spider, folk-token, folk-google-item, folk-social-post, folk-calendar, folk-map, folk-piano, folk-splat, folk-video-chat, folk-transcription, folk-image-gen, folk-video-gen, folk-blender, folk-freecad, folk-kicad, folk-itinerary, folk-destination, folk-budget, folk-packing-list, folk-booking' + 'folk-shape, folk-markdown, folk-wrapper, folk-slide, folk-chat, folk-obs-note, folk-rapp, folk-embed, folk-drawfast, folk-prompt, folk-zine-gen, folk-workflow-block, folk-choice-vote, folk-choice-rank, folk-choice-spider, folk-token, folk-google-item, folk-social-post, folk-calendar, folk-map, folk-piano, folk-splat, folk-video-chat, folk-transcription, folk-image-gen, folk-video-gen, folk-zine-gen, folk-blender, folk-freecad, folk-kicad, folk-itinerary, folk-destination, folk-budget, folk-packing-list, folk-booking' )]; shapes.sort((a, b) => {