Changeset 3428748
- Timestamp:
- 12/28/2025 10:27:38 PM (3 months ago)
- Location:
- kaigen
- Files:
-
- 22 edited
- 1 copied
-
tags/v0.2.7 (copied) (copied from kaigen/trunk)
-
tags/v0.2.7/.distignore (modified) (1 diff)
-
tags/v0.2.7/assets/kaigen-admin.css (modified) (3 diffs)
-
tags/v0.2.7/build/index.asset.php (modified) (1 diff)
-
tags/v0.2.7/build/index.js (modified) (1 diff)
-
tags/v0.2.7/inc/class-admin.php (modified) (2 diffs)
-
tags/v0.2.7/inc/class-image-provider.php (modified) (1 diff)
-
tags/v0.2.7/inc/class-rest-api.php (modified) (9 diffs)
-
tags/v0.2.7/inc/interface-image-provider.php (modified) (1 diff)
-
tags/v0.2.7/inc/providers/class-image-provider-openai.php (modified) (11 diffs)
-
tags/v0.2.7/inc/providers/class-image-provider-replicate.php (modified) (25 diffs)
-
tags/v0.2.7/kaigen.php (modified) (1 diff)
-
trunk/.distignore (modified) (1 diff)
-
trunk/assets/kaigen-admin.css (modified) (3 diffs)
-
trunk/build/index.asset.php (modified) (1 diff)
-
trunk/build/index.js (modified) (1 diff)
-
trunk/inc/class-admin.php (modified) (2 diffs)
-
trunk/inc/class-image-provider.php (modified) (1 diff)
-
trunk/inc/class-rest-api.php (modified) (9 diffs)
-
trunk/inc/interface-image-provider.php (modified) (1 diff)
-
trunk/inc/providers/class-image-provider-openai.php (modified) (11 diffs)
-
trunk/inc/providers/class-image-provider-replicate.php (modified) (25 diffs)
-
trunk/kaigen.php (modified) (1 diff)
Legend:
- Unmodified
- Added
- Removed
-
kaigen/tags/v0.2.7/.distignore
r3315977 r3428748 17 17 .gitignore 18 18 .gitattributes 19 CLAUDE.md20 19 README.md 21 20 -
kaigen/tags/v0.2.7/assets/kaigen-admin.css
r3424397 r3428748 87 87 } 88 88 89 /* ===== GENERATION PROGRESS ===== */ 90 91 .kaigen-modal__progress { 92 margin-top: 12px; 93 } 94 95 .kaigen-modal__progress-label { 96 font-size: 12px; 97 color: #555; 98 margin-bottom: 6px; 99 } 100 101 .kaigen-modal__progress-track { 102 height: 6px; 103 background: #e9eef0; 104 border-radius: 999px; 105 overflow: hidden; 106 border: 1px solid #c3c4c7; 107 } 108 109 .kaigen-modal__progress-fill { 110 height: 100%; 111 width: 0; 112 background: #007cba; 113 transition: width 0.2s linear; 114 } 115 116 .kaigen-generation-meta-loading { 117 margin: 8px 0 0 0; 118 color: #666; 119 font-size: 12px; 120 } 121 122 .kaigen-generation-meta-table { 123 width: 100%; 124 border-collapse: collapse; 125 margin-top: 8px; 126 font-size: 12px; 127 } 128 129 .kaigen-generation-meta-table th, 130 .kaigen-generation-meta-table td { 131 text-align: left; 132 padding: 4px 0; 133 vertical-align: top; 134 } 135 136 .kaigen-generation-meta-table th { 137 width: 90px; 138 color: #555; 139 font-weight: 600; 140 } 141 142 .kaigen-generation-meta-images { 143 display: flex; 144 flex-wrap: wrap; 145 gap: 6px; 146 } 147 148 .kaigen-generation-meta-image { 149 width: 40px; 150 height: 40px; 151 object-fit: cover; 152 border-radius: 4px; 153 border: 1px solid #ccd0d4; 154 } 155 156 .kaigen-progress-icon { 157 display: inline-flex; 158 align-items: center; 159 justify-content: center; 160 width: 20px; 161 height: 20px; 162 } 163 164 .kaigen-progress-icon__track { 165 width: 20px; 166 height: 6px; 167 background: #e9eef0; 168 border-radius: 999px; 169 overflow: hidden; 170 border: 1px solid #c3c4c7; 171 } 172 173 .kaigen-progress-icon__fill { 174 display: block; 175 height: 100%; 176 width: 0; 177 background: #007cba; 178 transition: width 0.2s linear; 179 } 180 89 181 /* ===== RESPONSIVE ADJUSTMENTS ===== */ 90 182 … … 124 216 125 217 .kaigen-modal__aspect-ratio-button-selected { 218 border: 2px solid #007cba; 219 background: #f0f8ff; 220 } 221 222 .kaigen-modal__quality-button { 223 cursor: pointer; 224 padding: 6px 10px; 225 border-radius: 4px; 226 border: 1px solid #ccd0d4; 227 background: #fff; 228 font-size: 12px; 229 } 230 231 .kaigen-modal__quality-button-selected { 126 232 border: 2px solid #007cba; 127 233 background: #f0f8ff; … … 250 356 } 251 357 358 .kaigen-modal-quality-container { 359 display: flex; 360 gap: 8px; 361 flex-wrap: wrap; 362 margin-top: 8px; 363 } 364 252 365 .kaigen-modal-aspect-ratio-icon-container { 253 366 width: 40px; -
kaigen/tags/v0.2.7/build/index.asset.php
r3424397 r3428748 1 <?php return array('dependencies' => array('react', 'wp-api-fetch', 'wp-block-editor', 'wp-components', 'wp-data', 'wp-element', 'wp-hooks', 'wp-rich-text'), 'version' => ' 99a220a8f58d5c236bd0');1 <?php return array('dependencies' => array('react', 'wp-api-fetch', 'wp-block-editor', 'wp-components', 'wp-data', 'wp-element', 'wp-hooks', 'wp-rich-text'), 'version' => 'ac234a5ba569d8e13376'); -
kaigen/tags/v0.2.7/build/index.js
r3424397 r3428748 1 (()=>{"use strict";var e={n:t=>{var a=t&&t.__esModule?()=>t.default:()=>t;return e.d(a,{a}),a},d:(t,a)=>{for(var r in a)e.o(a,r)&&!e.o(t,r)&&Object.defineProperty(t,r,{enumerable:!0,get:a[r]})},o:(e,t)=>Object.prototype.hasOwnProperty.call(e,t)};const t=window.React,a=window.wp.element,r=window.wp.components,n=async(e,t,a={})=>{try{const r=wp.data.select("core/editor")?.getEditorSettings()?.kaigen_provider;if(!r)throw new Error("No provider configured. Please check your plugin settings.");const n=wp.data.select("core/editor")?.getEditorSettings()?.kaigen_has_api_key;if(!n)throw new Error("No API key configured for the selected provider. Please add one in the KaiGen settings.");const i={prompt:e,provider:r};a.sourceImageUrls&&Array.isArray(a.sourceImageUrls)?i.source_image_urls=a.sourceImageUrls:a.sourceImageUrl&&(i.source_image_url=a.sourceImageUrl),a.additionalImageUrls&&Array.isArray(a.additionalImageUrls)&&(i.additional_image_urls=a.additionalImageUrls),a.maskUrl&&(i.mask_url=a.maskUrl),a.moderation&&["auto","low"].includes(a.moderation)&&(i.moderation=a.moderation),a.style&&["natural","vivid"].includes(a.style)&&(i.style=a.style),a.aspectRatio&&["1:1","16:9","9:16","4:3","3:4"].includes(a.aspectRatio)&&(i.aspect_ratio=a.aspectRatio);const o=await wp.apiFetch({path:"/kaigen/v1/generate-image",method:"POST",data:i});if(o.code&&o.message){if("content_moderation"===o.code)throw new Error(o.message);if("replicate_error"===o.code)throw new Error("Image generation failed: "+o.message);throw new Error(o.message)}if(!o||!o.url)throw new Error("Invalid response from server: "+JSON.stringify(o));o.id&&"number"==typeof o.id&&o.id>0?t({url:o.url,alt:e,id:o.id,caption:""}):t({url:o.url,alt:e,caption:""})}catch(e){t({error:e.message||"An unknown error occurred while generating the image"})}},i=window.kaiGen?.logoUrl,o=({isOpen:e,onClose:o,onSelect:l,initialReferenceImage:c})=>{const[s,m]=(0,a.useState)(""),[d,g]=(0,a.useState)(!1),[u,p]=(0,a.useState)(null),[k,b]=(0,a.useState)([]),[E,w]=(0,a.useState)([]),[h,f]=(0,a.useState)("1:1"),_="replicate"===(wp.data.select("core/editor")?.getEditorSettings()?.kaigen_provider||"replicate")?10:16;(0,a.useEffect)(()=>{e&&((async()=>{try{const e=await wp.apiFetch({path:"/kaigen/v1/reference-images",method:"GET"});return Array.isArray(e)?e:[]}catch(e){return[]}})().then(b),c&&c.url?w([c]):w([]))},[e,c]);const y=()=>{if(!s.trim())return void p("Please enter a prompt for image generation.");g(!0),p(null);const e={};E.length>0&&(e.sourceImageUrls=E.map(e=>e.url)),h&&(e.aspectRatio=h),n(s.trim(),e=>{e.error?(p(e.error),g(!1)):(l(e),g(!1),v())},e)},v=()=>{m(""),p(null),w([]),o()};return e?(0,t.createElement)(r.Modal,{className:"kaigen-modal",title:(0,t.createElement)("div",{className:"kaigen-modal__logo-container"},(0,t.createElement)("img",{src:i,alt:"KaiGen logo",className:"kaigen-modal__logo"})),"aria-label":"KaiGen",onRequestClose:v},u&&(0,t.createElement)("p",{className:"kaigen-error-text"},u),(0,t.createElement)("div",{className:"kaigen-modal__input-container"},(0,t.createElement)(r.Dropdown,{popoverProps:{placement:"bottom-start",focusOnMount:!0},renderToggle:({isOpen:e,onToggle:a})=>(0,t.createElement)(r.Button,{className:"kaigen-modal__ref-button "+(E.length>0?"kaigen-ref-button-selected":""),onClick:a,"aria-expanded":e,"aria-label":"Reference Images"},(0,t.createElement)(r.Dashicon,{icon:"format-image",className:E.length>0?"kaigen-ref-button-icon-selected":""})),renderContent:()=>{const e=c?[c,...k.filter(e=>e.id!==c.id)]:k;return(0,t.createElement)("div",{className:"kaigen-modal-dropdown-content-container"},(0,t.createElement)("h4",{className:"kaigen-modal-dropdown-content-title"},"Reference Images (up to ",_,")"),e.length>0?(0,t.createElement)("div",{className:"kaigen-modal-reference-images-container"},e.map((e,a)=>(0,t.createElement)("button",{type:"button",key:e.id||`initial-${a}`,onClick:()=>{w(t=>t.some(t=>t.url===e.url)?t.filter(t=>t.url!==e.url):t.length<_?[...t,e]:t)},className:"kaigen-modal-reference-image "+(E.some(t=>t.url===e.url)?"kaigen-modal-reference-image-selected":""),"aria-label":e.alt||"Select reference image"},(0,t.createElement)("img",{src:e.thumbnail_url||e.url,alt:e.alt||""})))):(0,t.createElement)("p",{className:"kaigen-modal-no-references"},"No reference images. Mark images in the Media Library to use them here."))}}),(0,t.createElement)("div",{className:"kaigen-modal__textarea-container"},(0,t.createElement)(r.TextareaControl,{className:"kaigen-modal__textarea",placeholder:"Image prompt...",value:s,onChange:m,onKeyDown:e=>{"Enter"!==e.key||e.shiftKey||(e.preventDefault(),y())},rows:2})),s.trim()&&(0,t.createElement)(r.Button,{className:"kaigen-modal__submit-button",variant:"primary",onClick:y,disabled:d||!s.trim(),"aria-label":"Generate Image"},d?(0,t.createElement)(r.Spinner,null):(0,t.createElement)(r.Dashicon,{icon:"admin-appearance"})),(0,t.createElement)(r.Dropdown,{popoverProps:{placement:"bottom-end",focusOnMount:!0},renderToggle:({isOpen:e,onToggle:a})=>(0,t.createElement)(r.Button,{className:"kaigen-modal__settings-button",onClick:a,"aria-expanded":e,"aria-label":"Settings"},(0,t.createElement)(r.Dashicon,{icon:"admin-generic"})),renderContent:()=>(0,t.createElement)("div",{className:"kaigen-modal-dropdown-content-container"},(0,t.createElement)("h4",{className:"kaigen-modal-dropdown-content-title"},"Aspect Ratio"),(0,t.createElement)("div",{className:"kaigen-modal-aspect-ratio-container"},[{value:"1:1",label:"1:1",title:"Square"},{value:"16:9",label:"16:9",title:"Landscape"},{value:"9:16",label:"9:16",title:"Portrait"}].map(e=>(0,t.createElement)("button",{type:"button",key:e.value,onClick:()=>f(t=>t===e.value?null:e.value),"aria-pressed":h===e.value,"aria-label":`${e.title} (${e.label})`,className:"kaigen-modal__aspect-ratio-button "+(h===e.value?"kaigen-modal__aspect-ratio-button-selected":"")},(0,t.createElement)("div",{className:"kaigen-modal-aspect-ratio-icon-container"},(0,t.createElement)("div",{className:`kaigen-modal-aspect-ratio-icon ${h===e.value?"kaigen-modal-aspect-ratio-icon-selected":""} kaigen-aspect-ratio-${e.value.replace(":","-")}`})),(0,t.createElement)("span",{className:"kaigen-modal-aspect-ratio-label"},e.label)))))}))):null},l=window.kaiGen?.logoUrl,c=({onSelect:e,shouldDisplay:n})=>{const[i,c]=(0,a.useState)(!1);return n?(0,t.createElement)(t.Fragment,null,(0,t.createElement)(r.Button,{onClick:()=>c(!0),className:"kaigen-placeholder-button","aria-label":"KaiGen"},(0,t.createElement)("img",{src:l,alt:"KaiGen",style:{width:"48px",height:"48px"}})),(0,t.createElement)("button",{type:"button",role:"menuitem",onClick:()=>c(!0),className:"components-button components-menu-item__button is-next-40px-default-size kaigen-menu-item-button"},(0,t.createElement)("span",{className:"components-menu-item__item"},"KaiGen"),(0,t.createElement)("img",{src:l,alt:"","aria-hidden":"true",className:"components-menu-items__item-icon has-icon-right",style:{width:"24px",height:"24px"}})),(0,t.createElement)(o,{isOpen:i,onClose:()=>c(!1),onSelect:e})):null},s=window.kaiGen?.logoUrl,m=({isGenerating:e,onGenerateImage:n,isRegenerating:i,onImageGenerated:l,isImageBlock:c,isTextSelected:m,currentImage:d})=>{const[g,u]=(0,a.useState)(!1);return c?(0,t.createElement)(t.Fragment,null,(0,t.createElement)(r.ToolbarGroup,null,(0,t.createElement)(r.ToolbarButton,{icon:i?(0,t.createElement)(r.Spinner,null):(0,t.createElement)("img",{src:s,alt:"KaiGen logo",className:"kaigen-toolbar-icon"}),label:i?"KaiGen is generating...":"KaiGen",onClick:()=>u(!0),disabled:i})),(0,t.createElement)(o,{isOpen:g,onClose:()=>u(!1),onSelect:l,initialReferenceImage:d})):m?(0,t.createElement)(r.ToolbarGroup,null,(0,t.createElement)(r.ToolbarButton,{icon:e?(0,t.createElement)(r.Spinner,null):"format-image",label:e?"KaiGen is generating...":"KaiGen",onClick:n,disabled:e})):null},d=window.wp.blockEditor,g=window.wp.data,u=window.wp.richText,p=({value:e})=>{const[r,i]=(0,a.useState)(!1),o=(0,g.useSelect)(e=>e("core/block-editor").getSelectedBlock(),[]),{replaceBlocks:l}=(0,g.useDispatch)("core/block-editor"),c=(0,a.useCallback)(()=>{if(o&&"core/paragraph"===o.name){const t=e.text.slice(e.start,e.end).trim();if(!t)return void wp.data.dispatch("core/notices").createErrorNotice("Please select some text to use as the image generation prompt.",{type:"snackbar"});const a=wp.data.select("core/editor")?.getEditorSettings()?.kaigen_provider;if(!a)return void wp.data.dispatch("core/notices").createErrorNotice("No AI provider configured. Please set one in the plugin settings.",{type:"snackbar"});const r=wp.blocks.createBlock("core/heading",{content:"Generating AI image...",level:2,className:"kaigen-text-center"});l(o.clientId,[r,o]),i(!0),n(t,e=>{if(i(!1),e.error)wp.data.dispatch("core/notices").createErrorNotice("Failed to generate image: "+e.error,{type:"snackbar"}),l(r.clientId,[]);else{const t={url:e.url,alt:e.alt,caption:""};e.id&&"number"==typeof e.id&&e.id>0&&(t.id=e.id);const a=wp.blocks.createBlock("core/image",t);l(r.clientId,[a])}})}},[o,e.text,e.start,e.end,l]),s=""!==e.text.slice(e.start,e.end).trim();return(0,t.createElement)(d.BlockControls,null,(0,t.createElement)(m,{isGenerating:r,onGenerateImage:c,isTextSelected:s}))};(0,u.registerFormatType)("kaigen/custom-format",{title:"AI Image Gen",tagName:"span",className:"kaigen-format",edit:({value:e})=>(0,t.createElement)(p,{value:e})});const k=window.wp.hooks;(0,k.addFilter)("editor.MediaUpload","kaigen/add-ai-tab",e=>a=>{const r=a.allowedTypes&&a.allowedTypes.includes("image")&&!a.multiple,n=wp.data.select("core/block-editor").getSelectedBlock(),i=n&&"core/image"===n.name,o=wp.data.select("core/editor")?.getEditorSettings()?.kaigen_has_api_key,l=r&&i&&!(n&&n.attributes&&n.attributes.url)&&o;return(0,t.createElement)(e,{...a,render:e=>(0,t.createElement)(t.Fragment,null,a.render(e),(0,t.createElement)(c,{onSelect:a.onSelect,shouldDisplay:l}))})});const b=window.wp.apiFetch;var E=e.n(b);(0,k.addFilter)("editor.BlockEdit","kaigen/add-regenerate-button",e=>n=>{if("core/image"!==n.name)return(0,t.createElement)(e,{...n});const i=n.attributes.id&&"number"==typeof n.attributes.id&&n.attributes.id>0,[o,l]=(0,a.useState)(!1),{attributes:{id:c,kaigen_reference_image:s},setAttributes:g}=n;(0,a.useEffect)(()=>{i&&!o&&(null==s?E()({path:`/wp/v2/media/${c}`}).then(e=>{if(e&&e.meta&&void 0!==e.meta.kaigen_reference_image){const t=!0===e.meta.kaigen_reference_image||1===e.meta.kaigen_reference_image;g({kaigen_reference_image:t})}l(!0)}).catch(()=>{l(!0)}):l(!0))},[i,c,s,o,g]);const u=n.attributes.url?{url:n.attributes.url,id:n.attributes.id,alt:n.attributes.alt||""}:null;return(0,t.createElement)(t.Fragment,null,(0,t.createElement)(e,{...n}),n.attributes.url&&(0,t.createElement)(d.BlockControls,null,(0,t.createElement)(m,{onImageGenerated:e=>{e.id&&"number"==typeof e.id&&e.id>0?n.setAttributes({url:e.url,id:e.id}):n.setAttributes({url:e.url,id:void 0}),wp.data.dispatch("core/notices").createSuccessNotice("Image generated successfully!",{type:"snackbar"})},isImageBlock:!0,currentImage:u})),i&&(0,t.createElement)(d.InspectorControls,null,(0,t.createElement)(r.PanelBody,{title:"KaiGen Settings",initialOpen:!1},(0,t.createElement)(r.CheckboxControl,{label:"Reference image",checked:!0===n.attributes.kaigen_reference_image,onChange:async e=>{const t=!0===e;n.setAttributes({kaigen_reference_image:t}),l(!0);try{await E()({path:`/wp/v2/media/${n.attributes.id}`,method:"POST",data:{meta:{kaigen_reference_image:t?1:0}}})}catch(e){wp.data.dispatch("core/notices").createErrorNotice("Failed to update reference image meta",{type:"snackbar"})}},help:"Add to the list of reference images."}))))}),(0,k.addFilter)("blocks.registerBlockType","kaigen/add-reference-image-attribute",(e,t)=>"core/image"!==t?e:{...e,attributes:{...e.attributes,kaigen_reference_image:{type:"boolean",default:!1}}})})();1 (()=>{"use strict";var e={n:t=>{var a=t&&t.__esModule?()=>t.default:()=>t;return e.d(a,{a}),a},d:(t,a)=>{for(var n in a)e.o(a,n)&&!e.o(t,n)&&Object.defineProperty(t,n,{enumerable:!0,get:a[n]})},o:(e,t)=>Object.prototype.hasOwnProperty.call(e,t)};const t=window.React,a=window.wp.element,n=window.wp.components,r=async(e,t,a={})=>{try{const n=wp.data.select("core/editor")?.getEditorSettings()?.kaigen_provider;if(!n)throw new Error("No provider configured. Please check your plugin settings.");const r=wp.data.select("core/editor")?.getEditorSettings()?.kaigen_has_api_key;if(!r)throw new Error("No API key configured for the selected provider. Please add one in the KaiGen settings.");const l={prompt:e,provider:n};a.sourceImageUrls&&Array.isArray(a.sourceImageUrls)?l.source_image_urls=a.sourceImageUrls:a.sourceImageUrl&&(l.source_image_url=a.sourceImageUrl),a.sourceImageIds&&Array.isArray(a.sourceImageIds)&&(l.source_image_ids=a.sourceImageIds),a.additionalImageUrls&&Array.isArray(a.additionalImageUrls)&&(l.additional_image_urls=a.additionalImageUrls),a.maskUrl&&(l.mask_url=a.maskUrl),a.moderation&&["auto","low"].includes(a.moderation)&&(l.moderation=a.moderation),a.style&&["natural","vivid"].includes(a.style)&&(l.style=a.style),a.aspectRatio&&["1:1","16:9","9:16","4:3","3:4"].includes(a.aspectRatio)&&(l.aspect_ratio=a.aspectRatio),a.quality&&["low","medium","high"].includes(a.quality)&&(l.quality=a.quality);const i=wp.apiFetch({path:"/kaigen/v1/estimated-generation-time",method:"POST",data:l});"function"==typeof a.onEstimatedTime&&i.then(e=>{e&&"number"==typeof e.estimated_time_seconds&&a.onEstimatedTime(e.estimated_time_seconds)}).catch(()=>{});const o=await wp.apiFetch({path:"/kaigen/v1/generate-image",method:"POST",data:l});if(o.code&&o.message){if("content_moderation"===o.code)throw new Error(o.message);if("replicate_error"===o.code)throw new Error("Image generation failed: "+o.message);throw new Error(o.message)}if(!o||!o.url)throw new Error("Invalid response from server: "+JSON.stringify(o));o.id&&"number"==typeof o.id&&o.id>0?t({url:o.url,alt:e,id:o.id,caption:""}):t({url:o.url,alt:e,caption:""})}catch(e){t({error:e.message||"An unknown error occurred while generating the image"})}},l=(e,t)=>{const[n,r]=(0,a.useState)(0),l=(0,a.useRef)(3e4),i=(0,a.useRef)(null),o=(0,a.useRef)(0);return(0,a.useEffect)(()=>{l.current="number"==typeof t&&t>0?t:3e4},[t]),(0,a.useEffect)(()=>{if(!e)return r(0),i.current=null,void(o.current=0);i.current||(i.current=Date.now(),r(0),o.current=0);const t=setInterval(()=>{const e=Date.now()-i.current,t=Math.min(Math.floor(e/l.current*100),99),a=Math.max(t,o.current);o.current=a,r(a)},200);return()=>clearInterval(t)},[e]),n},i=window.kaiGen?.logoUrl,o=({isOpen:e,onClose:o,onSelect:c,initialReferenceImage:s})=>{const[m,d]=(0,a.useState)(""),[u,g]=(0,a.useState)(!1),[p,k]=(0,a.useState)(null),[E,b]=(0,a.useState)([]),[h,f]=(0,a.useState)([]),[_,w]=(0,a.useState)("1:1"),[y,v]=(0,a.useState)("medium"),[N,I]=(0,a.useState)(null),S=wp.data.select("core/editor")?.getEditorSettings()||{},G=S.kaigen_provider||"replicate",C=S.kaigen_quality||"medium",T="replicate"===G?10:16,x=l(u,N);(0,a.useEffect)(()=>{e&&((async()=>{try{const e=await wp.apiFetch({path:"/kaigen/v1/reference-images",method:"GET"});return Array.isArray(e)?e:[]}catch(e){return[]}})().then(b),v(C),s&&s.url?f([s]):f([]))},[e,s,C]);const A=()=>{if(!m.trim())return void k("Please enter a prompt for image generation.");g(!0),I(null),k(null);const e={};h.length>0&&(e.sourceImageUrls=h.map(e=>e.url),e.sourceImageIds=h.map(e=>e.id).filter(e=>Number.isInteger(e)&&e>0)),_&&(e.aspectRatio=_),y&&(e.quality=y),e.onEstimatedTime=e=>{"number"==typeof e&&I(1e3*e)},r(m.trim(),e=>{e.error?(k(e.error),g(!1)):(c(e),g(!1),B())},e)},B=()=>{d(""),k(null),f([]),v(C),I(null),o()};return e?(0,t.createElement)(n.Modal,{className:"kaigen-modal",title:(0,t.createElement)("div",{className:"kaigen-modal__logo-container"},(0,t.createElement)("img",{src:i,alt:"KaiGen logo",className:"kaigen-modal__logo"})),"aria-label":"KaiGen",onRequestClose:B},p&&(0,t.createElement)("p",{className:"kaigen-error-text"},p),(0,t.createElement)("div",{className:"kaigen-modal__input-container"},(0,t.createElement)(n.Dropdown,{popoverProps:{placement:"bottom-start",focusOnMount:!0},renderToggle:({isOpen:e,onToggle:a})=>(0,t.createElement)(n.Button,{className:"kaigen-modal__ref-button "+(h.length>0?"kaigen-ref-button-selected":""),onClick:a,"aria-expanded":e,"aria-label":"Reference Images"},(0,t.createElement)(n.Dashicon,{icon:"format-image",className:h.length>0?"kaigen-ref-button-icon-selected":""})),renderContent:()=>{const e=s?[s,...E.filter(e=>e.id!==s.id)]:E;return(0,t.createElement)("div",{className:"kaigen-modal-dropdown-content-container"},(0,t.createElement)("h4",{className:"kaigen-modal-dropdown-content-title"},"Reference Images (up to ",T,")"),e.length>0?(0,t.createElement)("div",{className:"kaigen-modal-reference-images-container"},e.map((e,a)=>(0,t.createElement)("button",{type:"button",key:e.id||`initial-${a}`,onClick:()=>{f(t=>t.some(t=>t.url===e.url)?t.filter(t=>t.url!==e.url):t.length<T?[...t,e]:t)},className:"kaigen-modal-reference-image "+(h.some(t=>t.url===e.url)?"kaigen-modal-reference-image-selected":""),"aria-label":e.alt||"Select reference image"},(0,t.createElement)("img",{src:e.thumbnail_url||e.url,alt:e.alt||""})))):(0,t.createElement)("p",{className:"kaigen-modal-no-references"},"No reference images. Mark images in the Media Library to use them here."))}}),(0,t.createElement)("div",{className:"kaigen-modal__textarea-container"},(0,t.createElement)(n.TextareaControl,{className:"kaigen-modal__textarea",placeholder:"Image prompt...",value:m,onChange:d,onKeyDown:e=>{"Enter"!==e.key||e.shiftKey||(e.preventDefault(),A())},rows:2})),m.trim()&&(0,t.createElement)(n.Button,{className:"kaigen-modal__submit-button",variant:"primary",onClick:A,disabled:u||!m.trim(),"aria-label":"Generate Image"},(0,t.createElement)(n.Dashicon,{icon:"admin-appearance"})),(0,t.createElement)(n.Dropdown,{popoverProps:{placement:"bottom-end",focusOnMount:!0},renderToggle:({isOpen:e,onToggle:a})=>(0,t.createElement)(n.Button,{className:"kaigen-modal__settings-button",onClick:a,"aria-expanded":e,"aria-label":"Settings"},(0,t.createElement)(n.Dashicon,{icon:"admin-generic"})),renderContent:()=>(0,t.createElement)("div",{className:"kaigen-modal-dropdown-content-container"},(0,t.createElement)("h4",{className:"kaigen-modal-dropdown-content-title"},"Aspect Ratio"),(0,t.createElement)("div",{className:"kaigen-modal-aspect-ratio-container"},[{value:"1:1",label:"1:1",title:"Square"},{value:"16:9",label:"16:9",title:"Landscape"},{value:"9:16",label:"9:16",title:"Portrait"}].map(e=>(0,t.createElement)("button",{type:"button",key:e.value,onClick:()=>w(t=>t===e.value?null:e.value),"aria-pressed":_===e.value,"aria-label":`${e.title} (${e.label})`,className:"kaigen-modal__aspect-ratio-button "+(_===e.value?"kaigen-modal__aspect-ratio-button-selected":"")},(0,t.createElement)("div",{className:"kaigen-modal-aspect-ratio-icon-container"},(0,t.createElement)("div",{className:`kaigen-modal-aspect-ratio-icon ${_===e.value?"kaigen-modal-aspect-ratio-icon-selected":""} kaigen-aspect-ratio-${e.value.replace(":","-")}`})),(0,t.createElement)("span",{className:"kaigen-modal-aspect-ratio-label"},e.label)))),(0,t.createElement)("h4",{className:"kaigen-modal-dropdown-content-title"},"Quality"),(0,t.createElement)("div",{className:"kaigen-modal-quality-container"},[{value:"low",label:"Low"},{value:"medium",label:"Medium"},{value:"high",label:"High"}].map(e=>(0,t.createElement)("button",{type:"button",key:e.value,onClick:()=>v(e.value),"aria-pressed":y===e.value,className:"kaigen-modal__quality-button "+(y===e.value?"kaigen-modal__quality-button-selected":"")},e.label))))})),u&&(0,t.createElement)("div",{className:"kaigen-modal__progress"},(0,t.createElement)("div",{className:"kaigen-modal__progress-label"},"Generating... ",x,"%"),(0,t.createElement)("div",{className:"kaigen-modal__progress-track",role:"progressbar","aria-valuenow":x,"aria-valuemin":0,"aria-valuemax":100,"aria-label":"Image generation progress"},(0,t.createElement)("div",{className:"kaigen-modal__progress-fill",style:{width:`${x}%`}})))):null},c=window.kaiGen?.logoUrl,s=({onSelect:e,shouldDisplay:r})=>{const[l,i]=(0,a.useState)(!1);return r?(0,t.createElement)(t.Fragment,null,(0,t.createElement)(n.Button,{onClick:()=>i(!0),className:"kaigen-placeholder-button","aria-label":"KaiGen"},(0,t.createElement)("img",{src:c,alt:"KaiGen",style:{width:"48px",height:"48px"}})),(0,t.createElement)("button",{type:"button",role:"menuitem",onClick:()=>i(!0),className:"components-button components-menu-item__button is-next-40px-default-size kaigen-menu-item-button"},(0,t.createElement)("span",{className:"components-menu-item__item"},"KaiGen"),(0,t.createElement)("img",{src:c,alt:"","aria-hidden":"true",className:"components-menu-items__item-icon has-icon-right",style:{width:"24px",height:"24px"}})),(0,t.createElement)(o,{isOpen:l,onClose:()=>i(!1),onSelect:e})):null},m=window.kaiGen?.logoUrl,d=({isGenerating:e,onGenerateImage:r,isRegenerating:i,onImageGenerated:c,isImageBlock:s,isTextSelected:d,currentImage:u,estimatedDurationMs:g})=>{const[p,k]=(0,a.useState)(!1),E=l(e||i,g),b=(0,t.createElement)("span",{className:"kaigen-progress-icon","aria-hidden":"true"},(0,t.createElement)("span",{className:"kaigen-progress-icon__track"},(0,t.createElement)("span",{className:"kaigen-progress-icon__fill",style:{width:`${E}%`}})));return s?(0,t.createElement)(t.Fragment,null,(0,t.createElement)(n.ToolbarGroup,null,(0,t.createElement)(n.ToolbarButton,{icon:i?b:(0,t.createElement)("img",{src:m,alt:"KaiGen logo",className:"kaigen-toolbar-icon"}),label:i?`KaiGen is generating... ${E}%`:"KaiGen",onClick:()=>k(!0),disabled:i})),(0,t.createElement)(o,{isOpen:p,onClose:()=>k(!1),onSelect:c,initialReferenceImage:u})):d?(0,t.createElement)(n.ToolbarGroup,null,(0,t.createElement)(n.ToolbarButton,{icon:e?b:"format-image",label:e?`KaiGen is generating... ${E}%`:"KaiGen",onClick:r,disabled:e})):null},u=window.wp.blockEditor,g=window.wp.data,p=window.wp.richText,k=({value:e})=>{const[n,l]=(0,a.useState)(!1),[i,o]=(0,a.useState)(null),c=(0,g.useSelect)(e=>e("core/block-editor").getSelectedBlock(),[]),{replaceBlocks:s}=(0,g.useDispatch)("core/block-editor"),m=(0,a.useCallback)(()=>{if(c&&"core/paragraph"===c.name){const t=e.text.slice(e.start,e.end).trim();if(!t)return void wp.data.dispatch("core/notices").createErrorNotice("Please select some text to use as the image generation prompt.",{type:"snackbar"});const a=wp.data.select("core/editor")?.getEditorSettings()?.kaigen_provider;if(!a)return void wp.data.dispatch("core/notices").createErrorNotice("No AI provider configured. Please set one in the plugin settings.",{type:"snackbar"});const n=wp.blocks.createBlock("core/heading",{content:"Generating AI image...",level:2,className:"kaigen-text-center"});s(c.clientId,[n,c]),l(!0),o(null),r(t,e=>{if(l(!1),o(null),e.error)wp.data.dispatch("core/notices").createErrorNotice("Failed to generate image: "+e.error,{type:"snackbar"}),s(n.clientId,[]);else{const t={url:e.url,alt:e.alt,caption:""};e.id&&"number"==typeof e.id&&e.id>0&&(t.id=e.id);const a=wp.blocks.createBlock("core/image",t);s(n.clientId,[a])}},{onEstimatedTime:e=>{"number"==typeof e&&o(1e3*e)}})}},[c,e.text,e.start,e.end,s]),p=""!==e.text.slice(e.start,e.end).trim();return(0,t.createElement)(u.BlockControls,null,(0,t.createElement)(d,{isGenerating:n,onGenerateImage:m,isTextSelected:p,estimatedDurationMs:i}))};(0,p.registerFormatType)("kaigen/custom-format",{title:"AI Image Gen",tagName:"span",className:"kaigen-format",edit:({value:e})=>(0,t.createElement)(k,{value:e})});const E=window.wp.hooks;(0,E.addFilter)("editor.MediaUpload","kaigen/add-ai-tab",e=>a=>{const n=a.allowedTypes&&a.allowedTypes.includes("image")&&!a.multiple,r=wp.data.select("core/block-editor").getSelectedBlock(),l=r&&"core/image"===r.name,i=wp.data.select("core/editor")?.getEditorSettings()?.kaigen_has_api_key,o=n&&l&&!(r&&r.attributes&&r.attributes.url)&&i;return(0,t.createElement)(e,{...a,render:e=>(0,t.createElement)(t.Fragment,null,a.render(e),(0,t.createElement)(s,{onSelect:a.onSelect,shouldDisplay:o}))})});const b=window.wp.apiFetch;var h=e.n(b);(0,E.addFilter)("editor.BlockEdit","kaigen/add-regenerate-button",e=>r=>{if("core/image"!==r.name)return(0,t.createElement)(e,{...r});const l=r.attributes.id&&"number"==typeof r.attributes.id&&r.attributes.id>0,[i,o]=(0,a.useState)(!1),[c,s]=(0,a.useState)(null),[m,g]=(0,a.useState)(!1),[p,k]=(0,a.useState)([]),[E,b]=(0,a.useState)(!1),[f,_]=(0,a.useState)(null),{attributes:{id:w,kaigen_reference_image:y},setAttributes:v}=r;(0,a.useEffect)(()=>{l&&!i&&(null==y?h()({path:`/wp/v2/media/${w}`}).then(e=>{if(e&&e.meta&&void 0!==e.meta.kaigen_reference_image){const t=!0===e.meta.kaigen_reference_image||1===e.meta.kaigen_reference_image;v({kaigen_reference_image:t})}o(!0)}).catch(()=>{o(!0)}):o(!0))},[l,w,y,i,v]),(0,a.useEffect)(()=>{w&&(s(null),k([]),_(null))},[w]),(0,a.useEffect)(()=>{E&&w&&f!==w&&(g(!0),h()({path:`/kaigen/v1/generation-meta?attachment_id=${w}`}).then(e=>{s(e&&Object.keys(e).length?e:null)}).catch(()=>{s(null)}).finally(()=>{g(!1),_(w)}))},[E,w,f]),(0,a.useEffect)(()=>{if(!(E&&c&&Array.isArray(c.reference_image_ids)&&c.reference_image_ids.length))return void k([]);const e=c.reference_image_ids.join(",");h()({path:`/wp/v2/media?include=${e}&per_page=${c.reference_image_ids.length}`}).then(e=>{const t=Array.isArray(e)?e.map(e=>({id:e.id,url:e.media_details?.sizes?.thumbnail?.source_url||e.source_url})).filter(e=>e.url):[];k(t)}).catch(()=>{k([])})},[E,c]);const N=r.attributes.url?{url:r.attributes.url,id:r.attributes.id,alt:r.attributes.alt||""}:null;return(0,t.createElement)(t.Fragment,null,(0,t.createElement)(e,{...r}),r.attributes.url&&(0,t.createElement)(u.BlockControls,null,(0,t.createElement)(d,{onImageGenerated:e=>{e.id&&"number"==typeof e.id&&e.id>0?r.setAttributes({url:e.url,id:e.id}):r.setAttributes({url:e.url,id:void 0}),wp.data.dispatch("core/notices").createSuccessNotice("Image generated successfully!",{type:"snackbar"})},isImageBlock:!0,currentImage:N})),l&&(0,t.createElement)(u.InspectorControls,null,(0,t.createElement)(n.PanelBody,{title:"KaiGen Settings",initialOpen:!1,onToggle:e=>{b(e)}},(0,t.createElement)(n.CheckboxControl,{label:"Reference image",checked:!0===r.attributes.kaigen_reference_image,onChange:async e=>{const t=!0===e;r.setAttributes({kaigen_reference_image:t}),o(!0);try{await h()({path:`/wp/v2/media/${r.attributes.id}`,method:"POST",data:{meta:{kaigen_reference_image:t?1:0}}})}catch(e){wp.data.dispatch("core/notices").createErrorNotice("Failed to update reference image meta",{type:"snackbar"})}},help:"Add to the list of reference images."}),m&&(0,t.createElement)("p",{className:"kaigen-generation-meta-loading"},"Loading generation details..."),!m&&c&&(0,t.createElement)("table",{className:"kaigen-generation-meta-table"},(0,t.createElement)("tbody",null,(0,t.createElement)("tr",null,(0,t.createElement)("th",null,"Prompt"),(0,t.createElement)("td",null,c.prompt)),(0,t.createElement)("tr",null,(0,t.createElement)("th",null,"Provider"),(0,t.createElement)("td",null,c.provider)),(0,t.createElement)("tr",null,(0,t.createElement)("th",null,"Quality"),(0,t.createElement)("td",null,c.quality)),(0,t.createElement)("tr",null,(0,t.createElement)("th",null,"Model"),(0,t.createElement)("td",null,c.model)),p.length>0&&(0,t.createElement)("tr",null,(0,t.createElement)("th",null,"References"),(0,t.createElement)("td",null,(0,t.createElement)("div",{className:"kaigen-generation-meta-images"},p.map(e=>(0,t.createElement)("img",{key:e.id,src:e.url,alt:"",className:"kaigen-generation-meta-image"}))))))))))}),(0,E.addFilter)("blocks.registerBlockType","kaigen/add-reference-image-attribute",(e,t)=>"core/image"!==t?e:{...e,attributes:{...e.attributes,kaigen_reference_image:{type:"boolean",default:!1}}})})(); -
kaigen/tags/v0.2.7/inc/class-admin.php
r3424397 r3428748 94 94 } 95 95 96 $quality = Image_Provider::get_quality_setting(); 97 $provider_models = get_option( 'kaigen_provider_models', [] ); 98 $provider_model = $provider_models[ $provider ] ?? ''; 99 $estimated_time = 30; 100 101 $provider_instance = kaigen_provider_manager()->get_provider( $provider ); 102 if ( $provider_instance ) { 103 if ( ! empty( $provider_model ) ) { 104 $provider_instance->set_model( $provider_model ); 105 } 106 $estimated_time = (int) $provider_instance->get_estimated_generation_time( $quality, [] ); 107 } 108 96 109 // Add the provider setting in all possible locations to ensure it's available. 97 $settings['kaigen_provider'] = $provider; 110 $settings['kaigen_provider'] = $provider; 111 $settings['kaigen_quality'] = $quality; 112 $settings['kaigen_provider_model'] = $provider_model; 113 $settings['kaigen_estimated_generation_time_seconds'] = $estimated_time; 98 114 99 115 if ( ! isset( $settings['kaigen'] ) ) { 100 116 $settings['kaigen'] = []; 101 117 } 102 $settings['kaigen']['provider'] = $provider; 118 $settings['kaigen']['provider'] = $provider; 119 $settings['kaigen']['quality'] = $quality; 120 $settings['kaigen']['provider_model'] = $provider_model; 121 $settings['kaigen']['estimated_generation_time_seconds'] = $estimated_time; 103 122 104 123 // Add to editor settings directly. … … 106 125 $settings['kaigen_settings'] = []; 107 126 } 108 $settings['kaigen_settings']['provider'] = $provider; 109 $settings['kaigen_has_api_key'] = ! empty( $provider ) && ! empty( $api_keys[ $provider ] ); 127 $settings['kaigen_settings']['provider'] = $provider; 128 $settings['kaigen_settings']['quality'] = $quality; 129 $settings['kaigen_settings']['provider_model'] = $provider_model; 130 $settings['kaigen_settings']['estimated_generation_time_seconds'] = $estimated_time; 131 $settings['kaigen_has_api_key'] = ! empty( $provider ) && ! empty( $api_keys[ $provider ] ); 110 132 111 133 return $settings; -
kaigen/tags/v0.2.7/inc/class-image-provider.php
r3424397 r3428748 60 60 } 61 61 return false; 62 } 63 64 /** 65 * Gets the estimated image generation time in seconds. 66 * 67 * @param string $quality_setting Optional quality setting. 68 * @param array $additional_params Optional additional parameters for estimation. 69 * @return int Estimated time in seconds. 70 */ 71 public function get_estimated_generation_time( $quality_setting = '', $additional_params = [] ) { 72 return 30; 73 } 74 75 /** 76 * Resolves the model to use for a request. 77 * 78 * @param string $quality_setting Optional quality setting. 79 * @param array $additional_params Optional additional parameters for the request. 80 * @return string The resolved model identifier. 81 */ 82 public function get_model_for_request( $quality_setting = '', $additional_params = [] ) { 83 return $this->model; 62 84 } 63 85 -
kaigen/tags/v0.2.7/inc/class-rest-api.php
r3424397 r3428748 7 7 8 8 namespace KaiGen; 9 10 use WP_Error; 9 11 10 12 /** … … 99 101 ] 100 102 ); 103 104 // Register the estimated generation time endpoint. 105 register_rest_route( 106 self::API_NAMESPACE, 107 '/estimated-generation-time', 108 [ 109 'methods' => 'POST', 110 'callback' => [ $this, 'get_estimated_generation_time' ], 111 'permission_callback' => [ $this, 'check_permission' ], 112 ] 113 ); 114 115 // Register the generation metadata endpoint. 116 register_rest_route( 117 self::API_NAMESPACE, 118 '/generation-meta', 119 [ 120 'methods' => 'GET', 121 'callback' => [ $this, 'get_generation_meta' ], 122 'permission_callback' => [ $this, 'check_permission' ], 123 ] 124 ); 101 125 } 102 126 … … 121 145 $provider_id = $request->get_param( 'provider' ); 122 146 147 // Get additional parameters with defaults. 148 $additional_params = $this->get_additional_params( $request ); 149 123 150 // Get provider model. 124 $model = $this->get_provider_model( $provider_id );151 $model = $this->get_provider_model( $provider_id, $additional_params ); 125 152 if ( is_wp_error( $model ) ) { 126 153 return $model; 127 154 } 128 155 129 // Get additional parameters with defaults. 156 // Handle retries for image generation. 157 $response = $this->handle_generation_with_retries( $provider_id, $prompt, $model, $additional_params ); 158 159 if ( $response instanceof \WP_REST_Response ) { 160 $response_data = $response->get_data(); 161 if ( ! empty( $response_data['id'] ) ) { 162 $this->maybe_save_generation_meta( 163 absint( $response_data['id'] ), 164 $request, 165 $provider_id, 166 $model 167 ); 168 } 169 } 170 171 return $response; 172 } 173 174 /** 175 * Gets the estimated generation time for a request. 176 * 177 * @param WP_REST_Request $request The request object. 178 * @return WP_REST_Response|WP_Error The response or error. 179 */ 180 public function get_estimated_generation_time( $request ) { 181 $provider_id = $request->get_param( 'provider' ); 182 if ( empty( $provider_id ) ) { 183 return new WP_Error( 'invalid_provider', 'Provider is required.', [ 'status' => 400 ] ); 184 } 185 130 186 $additional_params = $this->get_additional_params( $request ); 131 132 // Handle retries for image generation. 133 return $this->handle_generation_with_retries( $provider_id, $prompt, $model, $additional_params ); 187 $model = $this->get_provider_model( $provider_id, $additional_params ); 188 if ( is_wp_error( $model ) ) { 189 return $model; 190 } 191 $quality = $additional_params['quality'] ?? Image_Provider::get_quality_setting(); 192 $provider = kaigen_provider_manager()->get_provider( $provider_id ); 193 194 if ( ! $provider ) { 195 return new WP_Error( 'invalid_provider', "Invalid provider: {$provider_id}", [ 'status' => 400 ] ); 196 } 197 198 $api_keys = get_option( 'kaigen_provider_api_keys', [] ); 199 $api_key = isset( $api_keys[ $provider_id ] ) ? $api_keys[ $provider_id ] : ''; 200 201 $provider_class = get_class( $provider ); 202 $provider_instance = new $provider_class( $api_key, $model ); 203 $estimated_time = (int) $provider_instance->get_estimated_generation_time( $quality, $additional_params ); 204 205 return new \WP_REST_Response( 206 [ 'estimated_time_seconds' => $estimated_time ], 207 200 208 ); 209 } 210 211 /** 212 * Gets the stored generation metadata for a post. 213 * 214 * @param WP_REST_Request $request The request object. 215 * @return WP_REST_Response|WP_Error The response or error. 216 */ 217 public function get_generation_meta( $request ) { 218 $attachment_id = absint( $request->get_param( 'attachment_id' ) ); 219 if ( ! $attachment_id ) { 220 return new WP_Error( 'invalid_attachment_id', 'Attachment ID is required.', [ 'status' => 400 ] ); 221 } 222 223 if ( ! current_user_can( 'edit_post', $attachment_id ) ) { 224 return new WP_Error( 'forbidden', 'You do not have permission to view this attachment.', [ 'status' => 403 ] ); 225 } 226 227 $meta = get_post_meta( $attachment_id, 'kaigen_generation_meta', true ); 228 if ( ! is_array( $meta ) ) { 229 $meta = []; 230 } 231 232 return new \WP_REST_Response( $meta, 200 ); 233 } 234 235 /** 236 * Saves generation metadata on the post when available. 237 * 238 * @param int $attachment_id The attachment ID. 239 * @param WP_REST_Request $request The request object. 240 * @param string $provider_id The provider ID. 241 * @param string $model The resolved model. 242 * @return void 243 */ 244 private function maybe_save_generation_meta( $attachment_id, $request, $provider_id, $model ) { 245 if ( ! $attachment_id ) { 246 return; 247 } 248 249 if ( ! current_user_can( 'edit_post', $attachment_id ) ) { 250 return; 251 } 252 253 $quality = $request->get_param( 'quality' ); 254 if ( ! in_array( $quality, [ 'low', 'medium', 'high' ], true ) ) { 255 $quality = Image_Provider::get_quality_setting(); 256 } 257 $meta = [ 258 'prompt' => sanitize_text_field( (string) $request->get_param( 'prompt' ) ), 259 'provider' => sanitize_text_field( $provider_id ), 260 'quality' => sanitize_text_field( $quality ), 261 'model' => sanitize_text_field( $model ), 262 ]; 263 $source_image_ids = $request->get_param( 'source_image_ids' ); 264 if ( is_array( $source_image_ids ) ) { 265 $sanitized_ids = array_values( 266 array_filter( 267 array_map( 'absint', $source_image_ids ) 268 ) 269 ); 270 if ( ! empty( $sanitized_ids ) ) { 271 $meta['reference_image_ids'] = $sanitized_ids; 272 } 273 } 274 275 update_post_meta( $attachment_id, 'kaigen_generation_meta', $meta ); 134 276 } 135 277 … … 138 280 * 139 281 * @param string $provider_id The provider ID. 282 * @param array $additional_params Additional parameters. 140 283 * @return string|WP_Error The model or error. 141 284 */ 142 private function get_provider_model( $provider_id ) { 143 // For Replicate, get the model based on quality setting. 144 if ( 'replicate' === $provider_id ) { 145 $quality = Image_Provider::get_quality_setting(); 146 147 $provider = kaigen_provider_manager()->get_provider( $provider_id ); 148 if ( $provider ) { 149 $model = $provider->get_model_from_quality_setting( $quality ); 150 return $model; 151 } 152 } 153 154 // For other providers, use the stored model or default. 285 private function get_provider_model( $provider_id, $additional_params = [] ) { 286 $provider = kaigen_provider_manager()->get_provider( $provider_id ); 287 if ( ! $provider ) { 288 return new WP_Error( 'invalid_provider', "Invalid provider: {$provider_id}", [ 'status' => 400 ] ); 289 } 290 155 291 $provider_models = get_option( 'kaigen_provider_models', [] ); 156 $default_models = [ 157 'openai' => 'dall-e-3', 158 ]; 159 160 if ( ! empty( $provider_models[ $provider_id ] ) ) { 161 return $provider_models[ $provider_id ]; 162 } 163 164 if ( ! empty( $default_models[ $provider_id ] ) ) { 165 $model = $default_models[ $provider_id ]; 166 $provider_models[ $provider_id ] = $model; 167 update_option( 'kaigen_provider_models', $provider_models ); 168 return $model; 169 } 170 171 return new \WP_Error( 'model_not_set', "No model set for provider: {$provider_id}", [ 'status' => 400 ] ); 292 $stored_model = $provider_models[ $provider_id ] ?? ''; 293 $quality = $additional_params['quality'] ?? Image_Provider::get_quality_setting(); 294 295 $api_keys = get_option( 'kaigen_provider_api_keys', [] ); 296 $api_key = isset( $api_keys[ $provider_id ] ) ? $api_keys[ $provider_id ] : ''; 297 298 $provider_class = get_class( $provider ); 299 $provider_instance = new $provider_class( $api_key, $stored_model ); 300 $model = $provider_instance->get_model_for_request( $quality, $additional_params ); 301 302 if ( empty( $model ) ) { 303 return new WP_Error( 'model_not_set', "No model set for provider: {$provider_id}", [ 'status' => 400 ] ); 304 } 305 306 return $model; 172 307 } 173 308 … … 184 319 $quality_settings = get_option( 'kaigen_quality_settings', [] ); 185 320 $style_value = isset( $quality_settings['style'] ) ? $quality_settings['style'] : 'natural'; 321 $quality_override = $request->get_param( 'quality' ); 322 if ( in_array( $quality_override, [ 'low', 'medium', 'high' ], true ) ) { 323 $quality = $quality_override; 324 } 186 325 187 326 $defaults = [ … … 201 340 $params[ $key ] = $request->get_param( $key ) ?? $default; 202 341 } 342 $params['quality'] = $quality; 203 343 204 344 // Add source image URL if provided (single or array). … … 276 416 if ( isset( $result['status'] ) && 'failed' === $result['status'] ) { 277 417 if ( isset( $result['error'] ) && strpos( $result['error'], 'flagged by safety filters' ) !== false ) { 278 return new \WP_Error(418 return new WP_Error( 279 419 'content_filtered', 280 420 'The image was flagged by the provider\'s safety filters. Please modify your prompt and try again.', … … 357 497 358 498 if ( $retry_count >= $max_retries ) { 359 return new \WP_Error(499 return new WP_Error( 360 500 'api_error', 361 501 'Failed after ' . $max_retries . ' attempts: ' . $e->getMessage(), … … 382 522 $provider = kaigen_provider_manager()->get_provider( $provider_id ); 383 523 if ( ! $provider ) { 384 return new \WP_Error( 'invalid_provider', "Invalid provider: {$provider_id}" );524 return new WP_Error( 'invalid_provider', "Invalid provider: {$provider_id}" ); 385 525 } 386 526 -
kaigen/tags/v0.2.7/inc/interface-image-provider.php
r3424397 r3428748 85 85 */ 86 86 public function set_model( $model ); 87 88 /** 89 * Gets the estimated image generation time in seconds. 90 * 91 * @param string $quality_setting Optional quality setting. 92 * @param array $additional_params Optional additional parameters for estimation. 93 * @return int Estimated time in seconds. 94 */ 95 public function get_estimated_generation_time( $quality_setting = '', $additional_params = [] ); 96 97 /** 98 * Resolves the model to use for a request. 99 * 100 * @param string $quality_setting Optional quality setting. 101 * @param array $additional_params Optional additional parameters for the request. 102 * @return string The resolved model identifier. 103 */ 104 public function get_model_for_request( $quality_setting = '', $additional_params = [] ); 87 105 } -
kaigen/tags/v0.2.7/inc/providers/class-image-provider-openai.php
r3424397 r3428748 9 9 10 10 use KaiGen\Image_Provider; 11 use WP_Error; 11 12 12 13 /** … … 73 74 74 75 $max_retries = 3; 75 $timeout = 150; // Increased timeout for image generation (docs say up to 2 mins).76 $timeout = 360; // Allow long-running high-quality generations. 76 77 $retry_delay = 2; // Seconds to wait between retries. 77 78 // Add filter to ensure WordPress respects our timeout settings.79 add_filter(80 'http_request_timeout',81 function () use ( $timeout ) {82 return $timeout;83 }84 );85 86 // Add filter to set cURL options.87 add_filter(88 'http_request_args',89 function ( $args ) use ( $timeout ) {90 $args['timeout'] = $timeout;91 $args['sslverify'] = true;92 $args['blocking'] = true;93 94 // Set cURL options directly.95 if ( ! isset( $args['curl'] ) ) {96 $args['curl'] = [];97 }98 $args['curl'][ CURLOPT_TIMEOUT ] = $timeout;99 $args['curl'][ CURLOPT_CONNECTTIMEOUT ] = 30; // Increased connect timeout.100 $args['curl'][ CURLOPT_TCP_KEEPALIVE ] = 1; // Enable TCP keepalive.101 102 // Adjust low speed settings to prevent timeouts on slow generation.103 $args['curl'][ CURLOPT_LOW_SPEED_TIME ] = 600; // Wait 10 minutes before timing out due to low speed.104 $args['curl'][ CURLOPT_LOW_SPEED_LIMIT ] = 1; // Only timeout if speed is effectively 0.105 106 return $args;107 }108 );109 78 110 79 // Default to API_BASE_URL. … … 116 85 } 117 86 118 // Get quality setting from admin options. 119 $quality = self::get_quality_setting(); 87 // Get quality setting from admin options or request override. 88 $quality = $additional_params['quality'] ?? self::get_quality_setting(); 89 90 // Scale timeout based on quality to keep faster requests snappy. 91 $timeout = 180; 92 if ( 'low' === $quality ) { 93 $timeout = 90; 94 } elseif ( 'high' === $quality ) { 95 $timeout = 360; 96 } 120 97 121 98 // Map quality settings to supported values. … … 156 133 $body .= $quality . "\r\n"; 157 134 135 // Add moderation parameter. 136 $body .= "--{$boundary}\r\n"; 137 $body .= 'Content-Disposition: form-data; name="moderation"' . "\r\n\r\n"; 138 $body .= "low\r\n"; 139 158 140 // Add format parameter (jpeg is faster than png). 159 141 $body .= "--{$boundary}\r\n"; … … 184 166 'prompt' => $prompt, 185 167 'quality' => $quality, 168 'moderation' => 'low', 186 169 'output_format' => 'jpeg', 187 170 ]; … … 197 180 198 181 // Make the API request with retries. 199 $attempt = 0; 200 $last_error = null; 182 $attempt = 0; 183 $last_error = null; 184 $final_response = null; 185 $final_error = null; 186 187 $curl_override = function ( $handle, $request_args, $url ) use ( $timeout ) { 188 if ( false === strpos( $url, 'api.openai.com' ) ) { 189 return; 190 } 191 192 // phpcs:ignore WordPress.WP.AlternativeFunctions.curl_curl_setopt -- Needed to prevent low-speed aborts for long-running requests. 193 curl_setopt( $handle, CURLOPT_TIMEOUT, $timeout ); 194 // phpcs:ignore WordPress.WP.AlternativeFunctions.curl_curl_setopt -- Needed to prevent low-speed aborts for long-running requests. 195 curl_setopt( $handle, CURLOPT_CONNECTTIMEOUT, 30 ); 196 // phpcs:ignore WordPress.WP.AlternativeFunctions.curl_curl_setopt -- Needed to prevent low-speed aborts for long-running requests. 197 curl_setopt( $handle, CURLOPT_LOW_SPEED_TIME, 180 ); 198 // phpcs:ignore WordPress.WP.AlternativeFunctions.curl_curl_setopt -- Needed to prevent low-speed aborts for long-running requests. 199 curl_setopt( $handle, CURLOPT_LOW_SPEED_LIMIT, 1 ); 200 }; 201 202 add_action( 'http_api_curl', $curl_override, 10, 3 ); 201 203 202 204 while ( $attempt < $max_retries ) { … … 224 226 } 225 227 226 // For other errors, return immediately.227 return $response;228 $final_error = new WP_Error( 'openai_error', 'OpenAI API request failed: ' . $error_message ); 229 break; 228 230 } 229 231 … … 232 234 233 235 if ( 200 !== $response_code ) { 234 235 236 // Parse error response. 236 237 $error_data = json_decode( $response_body, true ); … … 240 241 // Check for specific error about image URL in prompt. 241 242 if ( strpos( $error_message, 'image URL' ) !== false ) { 242 243 243 // Remove the image URL from the body and retry. 244 244 $body['prompt'] = $prompt; … … 257 257 258 258 if ( $retry_code < 400 ) { 259 return json_decode( $retry_body, true ); 259 $final_response = json_decode( $retry_body, true ); 260 break; 260 261 } 261 262 } 262 263 } 263 264 264 return new WP_Error( 'openai_error', $error_message ); 265 $final_error = new WP_Error( 'openai_error', $error_message ); 266 break; 265 267 } 266 268 267 return new WP_Error( 'api_error', "API Error (HTTP $response_code): $response_body" ); 269 $final_error = new WP_Error( 'api_error', "API Error (HTTP $response_code): $response_body" ); 270 break; 268 271 } 269 272 270 273 // Success! Return the response. 271 return json_decode( $response_body, true ); 272 } 273 274 // If we get here, all retries failed. 274 $final_response = json_decode( $response_body, true ); 275 break; 276 } 277 278 remove_action( 'http_api_curl', $curl_override, 10 ); 279 280 if ( null !== $final_response ) { 281 return $final_response; 282 } 283 284 if ( $final_error ) { 285 return $final_error; 286 } 287 275 288 if ( $last_error ) { 276 return $last_error;289 return new WP_Error( 'openai_error', 'OpenAI API request failed: ' . $last_error->get_error_message() ); 277 290 } 278 291 … … 363 376 self::DEFAULT_MODEL => 'GPT Image 1.5 (latest model)', 364 377 ]; 378 } 379 380 /** 381 * Gets the estimated image generation time in seconds. 382 * 383 * @param string $quality_setting Optional quality setting. 384 * @param array $additional_params Optional additional parameters for estimation. 385 * @return int Estimated time in seconds. 386 */ 387 public function get_estimated_generation_time( $quality_setting = '', $additional_params = [] ) { 388 $quality = $quality_setting ? $quality_setting : self::get_quality_setting(); 389 $has_source_images = ! empty( $additional_params['source_image_urls'] ) || 390 ! empty( $additional_params['source_image_url'] ) || 391 ! empty( $additional_params['additional_image_urls'] ); 392 393 switch ( $quality ) { 394 case 'low': 395 $base_time = 15; 396 break; 397 case 'high': 398 $base_time = 60; 399 break; 400 case 'medium': 401 default: 402 $base_time = 30; 403 break; 404 } 405 406 if ( $has_source_images ) { 407 return (int) ceil( $base_time * 1.25 ); 408 } 409 410 return $base_time; 411 } 412 413 /** 414 * Resolves the model to use for a request. 415 * 416 * @param string $quality_setting Optional quality setting. 417 * @param array $additional_params Optional additional parameters for the request. 418 * @return string The resolved model identifier. 419 */ 420 public function get_model_for_request( $quality_setting = '', $additional_params = [] ) { 421 return self::DEFAULT_MODEL; 365 422 } 366 423 -
kaigen/tags/v0.2.7/inc/providers/class-image-provider-replicate.php
r3424397 r3428748 9 9 10 10 use KaiGen\Image_Provider; 11 use WP_Error; 11 12 12 13 /** … … 78 79 79 80 $input_data = [ 'prompt' => $prompt ]; 80 81 // Determine which model to use.82 $model_to_use = $this->model;83 81 84 82 // Handle source image URLs (can be single string or array). … … 101 99 102 100 if ( ! empty( $image_inputs ) ) { 103 $model_to_use = $this->get_image_to_image_model();104 101 $input_data['image_input'] = $image_inputs; 105 102 106 103 // Set size to 2K for low quality image edits (seedream-4.5 only supports "2K", "4K", or "custom"). 107 $quality = self::get_quality_setting();104 $quality = $additional_params['quality'] ?? self::get_quality_setting(); 108 105 109 106 if ( 'low' === $quality ) { … … 153 150 ]; 154 151 155 $api_url = self::API_BASE_URL . "{$ model_to_use}/predictions";152 $api_url = self::API_BASE_URL . "{$this->model}/predictions"; 156 153 157 154 // Make initial request with shorter timeout since we're just waiting for the URL. … … 176 173 if ( null === $body && json_last_error() !== JSON_ERROR_NONE ) { 177 174 $raw_body = wp_remote_retrieve_body( $response ); 178 return new \WP_Error(175 return new WP_Error( 179 176 'replicate_api_error', 180 177 'Invalid JSON response from Replicate API. Response code: ' . $response_code . '. Body: ' . substr( $raw_body, 0, 200 ) … … 185 182 if ( ! is_array( $body ) ) { 186 183 $raw_body = wp_remote_retrieve_body( $response ); 187 return new \WP_Error(184 return new WP_Error( 188 185 'replicate_api_error', 189 186 'Unexpected response format from Replicate API. Response code: ' . $response_code . '. Body: ' . substr( $raw_body, 0, 200 ) … … 197 194 $error_message = 'Validation error: ' . ( is_array( $body['detail'] ) ? wp_json_encode( $body['detail'] ) : $body['detail'] ); 198 195 } 199 return new \WP_Error( 'replicate_validation_error', $error_message );196 return new WP_Error( 'replicate_validation_error', $error_message ); 200 197 } 201 198 … … 212 209 strpos( $error_message, 'E005' ) !== false 213 210 ) { 214 return new \WP_Error( 'content_moderation', 'Your prompt contains content that violates AI safety guidelines. Please try rephrasing it.' );211 return new WP_Error( 'content_moderation', 'Your prompt contains content that violates AI safety guidelines. Please try rephrasing it.' ); 215 212 } 216 213 217 214 // Return API errors immediately without retry. 218 return new \WP_Error( 'replicate_api_error', $error_message );215 return new WP_Error( 'replicate_api_error', $error_message ); 219 216 } 220 217 … … 261 258 $raw_body = wp_remote_retrieve_body( $response ); 262 259 $response_code = wp_remote_retrieve_response_code( $response ); 263 return new \WP_Error(260 return new WP_Error( 264 261 'replicate_api_error', 265 262 'Invalid JSON response from Replicate API when checking prediction status. Response code: ' . $response_code . '. Body: ' . substr( $raw_body, 0, 200 ) … … 271 268 $raw_body = wp_remote_retrieve_body( $response ); 272 269 $response_code = wp_remote_retrieve_response_code( $response ); 273 return new \WP_Error(270 return new WP_Error( 274 271 'replicate_api_error', 275 272 'Unexpected response format from Replicate API when checking prediction status. Response code: ' . $response_code . '. Body: ' . substr( $raw_body, 0, 200 ) … … 290 287 291 288 if ( ! is_array( $response ) ) { 292 return new \WP_Error( 'replicate_error', 'Invalid response format from Replicate' );289 return new WP_Error( 'replicate_error', 'Invalid response format from Replicate' ); 293 290 } 294 291 … … 306 303 strpos( $error_message, 'content moderation' ) !== false 307 304 ) { 308 return new \WP_Error(305 return new WP_Error( 309 306 'content_moderation', 310 307 'Your prompt contains content that violates AI safety guidelines. Please try rephrasing it.' … … 314 311 // Check for image-to-image specific errors. 315 312 if ( strpos( $error_message, 'image' ) !== false && strpos( $error_message, 'parameter' ) !== false ) { 316 return new \WP_Error(313 return new WP_Error( 317 314 'image_to_image_error', 318 315 'Image-to-image generation failed: ' . $error_message . '. Please check that your source image is valid and accessible.' … … 322 319 // Check for model-specific errors. 323 320 if ( strpos( $error_message, 'flux-kontext-pro' ) !== false || strpos( $error_message, 'model' ) !== false ) { 324 return new \WP_Error(321 return new WP_Error( 325 322 'model_error', 326 323 'Model error: ' . $error_message . '. The image-to-image model may be temporarily unavailable.' … … 329 326 330 327 // Return the raw error for other cases. 331 return new \WP_Error( 'replicate_error', $error_message );328 return new WP_Error( 'replicate_error', $error_message ); 332 329 } 333 330 … … 349 346 ) { 350 347 $error_message = 'Image-to-image generation failed. Please check that your source image is valid and accessible.'; 351 return new \WP_Error( 'image_to_image_failed', $error_message );348 return new WP_Error( 'image_to_image_failed', $error_message ); 352 349 } 353 350 … … 361 358 ) { 362 359 $error_message = 'Your prompt contains content that violates AI safety guidelines. Please try rephrasing it.'; 363 return new \WP_Error( 'content_moderation', $error_message );360 return new WP_Error( 'content_moderation', $error_message ); 364 361 } 365 362 … … 369 366 } 370 367 371 return new \WP_Error( 'generation_failed', $error_message );368 return new WP_Error( 'generation_failed', $error_message ); 372 369 } 373 370 … … 380 377 // Return pending error with prediction ID for polling. 381 378 if ( isset( $response['id'] ) ) { 382 return new \WP_Error(379 return new WP_Error( 383 380 'replicate_pending', 384 381 'Image generation is still processing', … … 387 384 } 388 385 389 return new \WP_Error( 'replicate_error', 'No image data in response' );386 return new WP_Error( 'replicate_error', 'No image data in response' ); 390 387 } 391 388 … … 408 405 return [ 409 406 'prunaai/hidream-l1-fast' => 'HiDream-I1 Fast by PrunaAI (low quality)', 410 'bytedance/seedream-4.5' => 'Seedream 4.5 by Bytedance ( highquality)',411 'google/nano-banana-pro' => 'Nano Banana Pro by Google (high estquality)',407 'bytedance/seedream-4.5' => 'Seedream 4.5 by Bytedance (medium quality)', 408 'google/nano-banana-pro' => 'Nano Banana Pro by Google (high quality)', 412 409 ]; 413 410 } 414 411 415 412 /** 413 * Gets the estimated image generation time in seconds. 414 * 415 * @param string $quality_setting Optional quality setting. 416 * @param array $additional_params Optional additional parameters for estimation. 417 * @return int Estimated time in seconds. 418 */ 419 public function get_estimated_generation_time( $quality_setting = '', $additional_params = [] ) { 420 $quality = $quality_setting ? $quality_setting : self::get_quality_setting(); 421 $model = $this->model ? $this->model : $this->get_model_from_quality_setting( $quality, $additional_params ); 422 $has_source_images = ! empty( $additional_params['source_image_urls'] ) || 423 ! empty( $additional_params['source_image_url'] ); 424 425 switch ( $model ) { 426 case 'prunaai/hidream-l1-fast': 427 $base_time = 3; 428 break; 429 case 'bytedance/seedream-4.5': 430 $base_time = 20; 431 break; 432 case 'google/nano-banana-pro': 433 $base_time = 40; 434 break; 435 default: 436 $base_time = 30; 437 break; 438 } 439 440 if ( $has_source_images ) { 441 return (int) ceil( $base_time * 1.25 ); 442 } 443 444 return $base_time; 445 } 446 447 /** 448 * Resolves the model to use for a request. 449 * 450 * @param string $quality_setting Optional quality setting. 451 * @param array $additional_params Optional additional parameters for the request. 452 * @return string The resolved model identifier. 453 */ 454 public function get_model_for_request( $quality_setting = '', $additional_params = [] ) { 455 $quality = $quality_setting ? $quality_setting : self::get_quality_setting(); 456 return $this->model ? $this->model : $this->get_model_from_quality_setting( $quality, $additional_params ); 457 } 458 459 /** 416 460 * Gets the image-to-image model for Replicate based on quality setting. 417 461 * 462 * @param string $quality_setting The quality setting. 418 463 * @return string The image-to-image model. 419 464 */ 420 private function get_image_to_image_model( ) {465 private function get_image_to_image_model( $quality_setting ) { 421 466 $model = 'bytedance/seedream-4.5'; 422 $quality = self::get_quality_setting();467 $quality = $quality_setting ? $quality_setting : self::get_quality_setting(); 423 468 424 469 if ( 'high' === $quality ) { … … 433 478 * 434 479 * @param string $quality_setting The quality setting. 480 * @param array $additional_params Optional additional parameters for the request. 435 481 * @return string The model. 436 482 */ 437 public function get_model_from_quality_setting( $quality_setting ) { 483 public function get_model_from_quality_setting( $quality_setting, $additional_params = [] ) { 484 $has_source_images = ! empty( $additional_params['source_image_urls'] ) || 485 ! empty( $additional_params['source_image_url'] ); 486 487 if ( $has_source_images ) { 488 return $this->get_image_to_image_model( $quality_setting ); 489 } 490 438 491 switch ( $quality_setting ) { 439 492 case 'low': … … 465 518 if ( is_wp_error( $head_response ) ) { 466 519 $error_message = 'Image URL not accessible: ' . $head_response->get_error_message(); 467 return new \WP_Error( 'image_not_accessible', $error_message );520 return new WP_Error( 'image_not_accessible', $error_message ); 468 521 } 469 522 … … 493 546 if ( is_wp_error( $response ) ) { 494 547 $error_message = 'Failed to download image: ' . $response->get_error_message(); 495 return new \WP_Error( 'image_download_failed', $error_message );548 return new WP_Error( 'image_download_failed', $error_message ); 496 549 } 497 550 … … 499 552 if ( 200 !== $response_code ) { 500 553 $error_message = "Failed to download image: HTTP {$response_code}"; 501 return new \WP_Error( 'image_download_failed', $error_message );554 return new WP_Error( 'image_download_failed', $error_message ); 502 555 } 503 556 504 557 $image_data = wp_remote_retrieve_body( $response ); 505 558 if ( empty( $image_data ) ) { 506 return new \WP_Error( 'empty_image_data', 'Downloaded image data is empty' );559 return new WP_Error( 'empty_image_data', 'Downloaded image data is empty' ); 507 560 } 508 561 -
kaigen/tags/v0.2.7/kaigen.php
r3424397 r3428748 5 5 * Requires at least: 6.1 6 6 * Requires PHP: 7.0 7 * Version: 0.2. 67 * Version: 0.2.7 8 8 * Author: Jacob Schweitzer 9 9 * License: GPL-2.0-or-later -
kaigen/trunk/.distignore
r3315977 r3428748 17 17 .gitignore 18 18 .gitattributes 19 CLAUDE.md20 19 README.md 21 20 -
kaigen/trunk/assets/kaigen-admin.css
r3424397 r3428748 87 87 } 88 88 89 /* ===== GENERATION PROGRESS ===== */ 90 91 .kaigen-modal__progress { 92 margin-top: 12px; 93 } 94 95 .kaigen-modal__progress-label { 96 font-size: 12px; 97 color: #555; 98 margin-bottom: 6px; 99 } 100 101 .kaigen-modal__progress-track { 102 height: 6px; 103 background: #e9eef0; 104 border-radius: 999px; 105 overflow: hidden; 106 border: 1px solid #c3c4c7; 107 } 108 109 .kaigen-modal__progress-fill { 110 height: 100%; 111 width: 0; 112 background: #007cba; 113 transition: width 0.2s linear; 114 } 115 116 .kaigen-generation-meta-loading { 117 margin: 8px 0 0 0; 118 color: #666; 119 font-size: 12px; 120 } 121 122 .kaigen-generation-meta-table { 123 width: 100%; 124 border-collapse: collapse; 125 margin-top: 8px; 126 font-size: 12px; 127 } 128 129 .kaigen-generation-meta-table th, 130 .kaigen-generation-meta-table td { 131 text-align: left; 132 padding: 4px 0; 133 vertical-align: top; 134 } 135 136 .kaigen-generation-meta-table th { 137 width: 90px; 138 color: #555; 139 font-weight: 600; 140 } 141 142 .kaigen-generation-meta-images { 143 display: flex; 144 flex-wrap: wrap; 145 gap: 6px; 146 } 147 148 .kaigen-generation-meta-image { 149 width: 40px; 150 height: 40px; 151 object-fit: cover; 152 border-radius: 4px; 153 border: 1px solid #ccd0d4; 154 } 155 156 .kaigen-progress-icon { 157 display: inline-flex; 158 align-items: center; 159 justify-content: center; 160 width: 20px; 161 height: 20px; 162 } 163 164 .kaigen-progress-icon__track { 165 width: 20px; 166 height: 6px; 167 background: #e9eef0; 168 border-radius: 999px; 169 overflow: hidden; 170 border: 1px solid #c3c4c7; 171 } 172 173 .kaigen-progress-icon__fill { 174 display: block; 175 height: 100%; 176 width: 0; 177 background: #007cba; 178 transition: width 0.2s linear; 179 } 180 89 181 /* ===== RESPONSIVE ADJUSTMENTS ===== */ 90 182 … … 124 216 125 217 .kaigen-modal__aspect-ratio-button-selected { 218 border: 2px solid #007cba; 219 background: #f0f8ff; 220 } 221 222 .kaigen-modal__quality-button { 223 cursor: pointer; 224 padding: 6px 10px; 225 border-radius: 4px; 226 border: 1px solid #ccd0d4; 227 background: #fff; 228 font-size: 12px; 229 } 230 231 .kaigen-modal__quality-button-selected { 126 232 border: 2px solid #007cba; 127 233 background: #f0f8ff; … … 250 356 } 251 357 358 .kaigen-modal-quality-container { 359 display: flex; 360 gap: 8px; 361 flex-wrap: wrap; 362 margin-top: 8px; 363 } 364 252 365 .kaigen-modal-aspect-ratio-icon-container { 253 366 width: 40px; -
kaigen/trunk/build/index.asset.php
r3424397 r3428748 1 <?php return array('dependencies' => array('react', 'wp-api-fetch', 'wp-block-editor', 'wp-components', 'wp-data', 'wp-element', 'wp-hooks', 'wp-rich-text'), 'version' => ' 99a220a8f58d5c236bd0');1 <?php return array('dependencies' => array('react', 'wp-api-fetch', 'wp-block-editor', 'wp-components', 'wp-data', 'wp-element', 'wp-hooks', 'wp-rich-text'), 'version' => 'ac234a5ba569d8e13376'); -
kaigen/trunk/build/index.js
r3424397 r3428748 1 (()=>{"use strict";var e={n:t=>{var a=t&&t.__esModule?()=>t.default:()=>t;return e.d(a,{a}),a},d:(t,a)=>{for(var r in a)e.o(a,r)&&!e.o(t,r)&&Object.defineProperty(t,r,{enumerable:!0,get:a[r]})},o:(e,t)=>Object.prototype.hasOwnProperty.call(e,t)};const t=window.React,a=window.wp.element,r=window.wp.components,n=async(e,t,a={})=>{try{const r=wp.data.select("core/editor")?.getEditorSettings()?.kaigen_provider;if(!r)throw new Error("No provider configured. Please check your plugin settings.");const n=wp.data.select("core/editor")?.getEditorSettings()?.kaigen_has_api_key;if(!n)throw new Error("No API key configured for the selected provider. Please add one in the KaiGen settings.");const i={prompt:e,provider:r};a.sourceImageUrls&&Array.isArray(a.sourceImageUrls)?i.source_image_urls=a.sourceImageUrls:a.sourceImageUrl&&(i.source_image_url=a.sourceImageUrl),a.additionalImageUrls&&Array.isArray(a.additionalImageUrls)&&(i.additional_image_urls=a.additionalImageUrls),a.maskUrl&&(i.mask_url=a.maskUrl),a.moderation&&["auto","low"].includes(a.moderation)&&(i.moderation=a.moderation),a.style&&["natural","vivid"].includes(a.style)&&(i.style=a.style),a.aspectRatio&&["1:1","16:9","9:16","4:3","3:4"].includes(a.aspectRatio)&&(i.aspect_ratio=a.aspectRatio);const o=await wp.apiFetch({path:"/kaigen/v1/generate-image",method:"POST",data:i});if(o.code&&o.message){if("content_moderation"===o.code)throw new Error(o.message);if("replicate_error"===o.code)throw new Error("Image generation failed: "+o.message);throw new Error(o.message)}if(!o||!o.url)throw new Error("Invalid response from server: "+JSON.stringify(o));o.id&&"number"==typeof o.id&&o.id>0?t({url:o.url,alt:e,id:o.id,caption:""}):t({url:o.url,alt:e,caption:""})}catch(e){t({error:e.message||"An unknown error occurred while generating the image"})}},i=window.kaiGen?.logoUrl,o=({isOpen:e,onClose:o,onSelect:l,initialReferenceImage:c})=>{const[s,m]=(0,a.useState)(""),[d,g]=(0,a.useState)(!1),[u,p]=(0,a.useState)(null),[k,b]=(0,a.useState)([]),[E,w]=(0,a.useState)([]),[h,f]=(0,a.useState)("1:1"),_="replicate"===(wp.data.select("core/editor")?.getEditorSettings()?.kaigen_provider||"replicate")?10:16;(0,a.useEffect)(()=>{e&&((async()=>{try{const e=await wp.apiFetch({path:"/kaigen/v1/reference-images",method:"GET"});return Array.isArray(e)?e:[]}catch(e){return[]}})().then(b),c&&c.url?w([c]):w([]))},[e,c]);const y=()=>{if(!s.trim())return void p("Please enter a prompt for image generation.");g(!0),p(null);const e={};E.length>0&&(e.sourceImageUrls=E.map(e=>e.url)),h&&(e.aspectRatio=h),n(s.trim(),e=>{e.error?(p(e.error),g(!1)):(l(e),g(!1),v())},e)},v=()=>{m(""),p(null),w([]),o()};return e?(0,t.createElement)(r.Modal,{className:"kaigen-modal",title:(0,t.createElement)("div",{className:"kaigen-modal__logo-container"},(0,t.createElement)("img",{src:i,alt:"KaiGen logo",className:"kaigen-modal__logo"})),"aria-label":"KaiGen",onRequestClose:v},u&&(0,t.createElement)("p",{className:"kaigen-error-text"},u),(0,t.createElement)("div",{className:"kaigen-modal__input-container"},(0,t.createElement)(r.Dropdown,{popoverProps:{placement:"bottom-start",focusOnMount:!0},renderToggle:({isOpen:e,onToggle:a})=>(0,t.createElement)(r.Button,{className:"kaigen-modal__ref-button "+(E.length>0?"kaigen-ref-button-selected":""),onClick:a,"aria-expanded":e,"aria-label":"Reference Images"},(0,t.createElement)(r.Dashicon,{icon:"format-image",className:E.length>0?"kaigen-ref-button-icon-selected":""})),renderContent:()=>{const e=c?[c,...k.filter(e=>e.id!==c.id)]:k;return(0,t.createElement)("div",{className:"kaigen-modal-dropdown-content-container"},(0,t.createElement)("h4",{className:"kaigen-modal-dropdown-content-title"},"Reference Images (up to ",_,")"),e.length>0?(0,t.createElement)("div",{className:"kaigen-modal-reference-images-container"},e.map((e,a)=>(0,t.createElement)("button",{type:"button",key:e.id||`initial-${a}`,onClick:()=>{w(t=>t.some(t=>t.url===e.url)?t.filter(t=>t.url!==e.url):t.length<_?[...t,e]:t)},className:"kaigen-modal-reference-image "+(E.some(t=>t.url===e.url)?"kaigen-modal-reference-image-selected":""),"aria-label":e.alt||"Select reference image"},(0,t.createElement)("img",{src:e.thumbnail_url||e.url,alt:e.alt||""})))):(0,t.createElement)("p",{className:"kaigen-modal-no-references"},"No reference images. Mark images in the Media Library to use them here."))}}),(0,t.createElement)("div",{className:"kaigen-modal__textarea-container"},(0,t.createElement)(r.TextareaControl,{className:"kaigen-modal__textarea",placeholder:"Image prompt...",value:s,onChange:m,onKeyDown:e=>{"Enter"!==e.key||e.shiftKey||(e.preventDefault(),y())},rows:2})),s.trim()&&(0,t.createElement)(r.Button,{className:"kaigen-modal__submit-button",variant:"primary",onClick:y,disabled:d||!s.trim(),"aria-label":"Generate Image"},d?(0,t.createElement)(r.Spinner,null):(0,t.createElement)(r.Dashicon,{icon:"admin-appearance"})),(0,t.createElement)(r.Dropdown,{popoverProps:{placement:"bottom-end",focusOnMount:!0},renderToggle:({isOpen:e,onToggle:a})=>(0,t.createElement)(r.Button,{className:"kaigen-modal__settings-button",onClick:a,"aria-expanded":e,"aria-label":"Settings"},(0,t.createElement)(r.Dashicon,{icon:"admin-generic"})),renderContent:()=>(0,t.createElement)("div",{className:"kaigen-modal-dropdown-content-container"},(0,t.createElement)("h4",{className:"kaigen-modal-dropdown-content-title"},"Aspect Ratio"),(0,t.createElement)("div",{className:"kaigen-modal-aspect-ratio-container"},[{value:"1:1",label:"1:1",title:"Square"},{value:"16:9",label:"16:9",title:"Landscape"},{value:"9:16",label:"9:16",title:"Portrait"}].map(e=>(0,t.createElement)("button",{type:"button",key:e.value,onClick:()=>f(t=>t===e.value?null:e.value),"aria-pressed":h===e.value,"aria-label":`${e.title} (${e.label})`,className:"kaigen-modal__aspect-ratio-button "+(h===e.value?"kaigen-modal__aspect-ratio-button-selected":"")},(0,t.createElement)("div",{className:"kaigen-modal-aspect-ratio-icon-container"},(0,t.createElement)("div",{className:`kaigen-modal-aspect-ratio-icon ${h===e.value?"kaigen-modal-aspect-ratio-icon-selected":""} kaigen-aspect-ratio-${e.value.replace(":","-")}`})),(0,t.createElement)("span",{className:"kaigen-modal-aspect-ratio-label"},e.label)))))}))):null},l=window.kaiGen?.logoUrl,c=({onSelect:e,shouldDisplay:n})=>{const[i,c]=(0,a.useState)(!1);return n?(0,t.createElement)(t.Fragment,null,(0,t.createElement)(r.Button,{onClick:()=>c(!0),className:"kaigen-placeholder-button","aria-label":"KaiGen"},(0,t.createElement)("img",{src:l,alt:"KaiGen",style:{width:"48px",height:"48px"}})),(0,t.createElement)("button",{type:"button",role:"menuitem",onClick:()=>c(!0),className:"components-button components-menu-item__button is-next-40px-default-size kaigen-menu-item-button"},(0,t.createElement)("span",{className:"components-menu-item__item"},"KaiGen"),(0,t.createElement)("img",{src:l,alt:"","aria-hidden":"true",className:"components-menu-items__item-icon has-icon-right",style:{width:"24px",height:"24px"}})),(0,t.createElement)(o,{isOpen:i,onClose:()=>c(!1),onSelect:e})):null},s=window.kaiGen?.logoUrl,m=({isGenerating:e,onGenerateImage:n,isRegenerating:i,onImageGenerated:l,isImageBlock:c,isTextSelected:m,currentImage:d})=>{const[g,u]=(0,a.useState)(!1);return c?(0,t.createElement)(t.Fragment,null,(0,t.createElement)(r.ToolbarGroup,null,(0,t.createElement)(r.ToolbarButton,{icon:i?(0,t.createElement)(r.Spinner,null):(0,t.createElement)("img",{src:s,alt:"KaiGen logo",className:"kaigen-toolbar-icon"}),label:i?"KaiGen is generating...":"KaiGen",onClick:()=>u(!0),disabled:i})),(0,t.createElement)(o,{isOpen:g,onClose:()=>u(!1),onSelect:l,initialReferenceImage:d})):m?(0,t.createElement)(r.ToolbarGroup,null,(0,t.createElement)(r.ToolbarButton,{icon:e?(0,t.createElement)(r.Spinner,null):"format-image",label:e?"KaiGen is generating...":"KaiGen",onClick:n,disabled:e})):null},d=window.wp.blockEditor,g=window.wp.data,u=window.wp.richText,p=({value:e})=>{const[r,i]=(0,a.useState)(!1),o=(0,g.useSelect)(e=>e("core/block-editor").getSelectedBlock(),[]),{replaceBlocks:l}=(0,g.useDispatch)("core/block-editor"),c=(0,a.useCallback)(()=>{if(o&&"core/paragraph"===o.name){const t=e.text.slice(e.start,e.end).trim();if(!t)return void wp.data.dispatch("core/notices").createErrorNotice("Please select some text to use as the image generation prompt.",{type:"snackbar"});const a=wp.data.select("core/editor")?.getEditorSettings()?.kaigen_provider;if(!a)return void wp.data.dispatch("core/notices").createErrorNotice("No AI provider configured. Please set one in the plugin settings.",{type:"snackbar"});const r=wp.blocks.createBlock("core/heading",{content:"Generating AI image...",level:2,className:"kaigen-text-center"});l(o.clientId,[r,o]),i(!0),n(t,e=>{if(i(!1),e.error)wp.data.dispatch("core/notices").createErrorNotice("Failed to generate image: "+e.error,{type:"snackbar"}),l(r.clientId,[]);else{const t={url:e.url,alt:e.alt,caption:""};e.id&&"number"==typeof e.id&&e.id>0&&(t.id=e.id);const a=wp.blocks.createBlock("core/image",t);l(r.clientId,[a])}})}},[o,e.text,e.start,e.end,l]),s=""!==e.text.slice(e.start,e.end).trim();return(0,t.createElement)(d.BlockControls,null,(0,t.createElement)(m,{isGenerating:r,onGenerateImage:c,isTextSelected:s}))};(0,u.registerFormatType)("kaigen/custom-format",{title:"AI Image Gen",tagName:"span",className:"kaigen-format",edit:({value:e})=>(0,t.createElement)(p,{value:e})});const k=window.wp.hooks;(0,k.addFilter)("editor.MediaUpload","kaigen/add-ai-tab",e=>a=>{const r=a.allowedTypes&&a.allowedTypes.includes("image")&&!a.multiple,n=wp.data.select("core/block-editor").getSelectedBlock(),i=n&&"core/image"===n.name,o=wp.data.select("core/editor")?.getEditorSettings()?.kaigen_has_api_key,l=r&&i&&!(n&&n.attributes&&n.attributes.url)&&o;return(0,t.createElement)(e,{...a,render:e=>(0,t.createElement)(t.Fragment,null,a.render(e),(0,t.createElement)(c,{onSelect:a.onSelect,shouldDisplay:l}))})});const b=window.wp.apiFetch;var E=e.n(b);(0,k.addFilter)("editor.BlockEdit","kaigen/add-regenerate-button",e=>n=>{if("core/image"!==n.name)return(0,t.createElement)(e,{...n});const i=n.attributes.id&&"number"==typeof n.attributes.id&&n.attributes.id>0,[o,l]=(0,a.useState)(!1),{attributes:{id:c,kaigen_reference_image:s},setAttributes:g}=n;(0,a.useEffect)(()=>{i&&!o&&(null==s?E()({path:`/wp/v2/media/${c}`}).then(e=>{if(e&&e.meta&&void 0!==e.meta.kaigen_reference_image){const t=!0===e.meta.kaigen_reference_image||1===e.meta.kaigen_reference_image;g({kaigen_reference_image:t})}l(!0)}).catch(()=>{l(!0)}):l(!0))},[i,c,s,o,g]);const u=n.attributes.url?{url:n.attributes.url,id:n.attributes.id,alt:n.attributes.alt||""}:null;return(0,t.createElement)(t.Fragment,null,(0,t.createElement)(e,{...n}),n.attributes.url&&(0,t.createElement)(d.BlockControls,null,(0,t.createElement)(m,{onImageGenerated:e=>{e.id&&"number"==typeof e.id&&e.id>0?n.setAttributes({url:e.url,id:e.id}):n.setAttributes({url:e.url,id:void 0}),wp.data.dispatch("core/notices").createSuccessNotice("Image generated successfully!",{type:"snackbar"})},isImageBlock:!0,currentImage:u})),i&&(0,t.createElement)(d.InspectorControls,null,(0,t.createElement)(r.PanelBody,{title:"KaiGen Settings",initialOpen:!1},(0,t.createElement)(r.CheckboxControl,{label:"Reference image",checked:!0===n.attributes.kaigen_reference_image,onChange:async e=>{const t=!0===e;n.setAttributes({kaigen_reference_image:t}),l(!0);try{await E()({path:`/wp/v2/media/${n.attributes.id}`,method:"POST",data:{meta:{kaigen_reference_image:t?1:0}}})}catch(e){wp.data.dispatch("core/notices").createErrorNotice("Failed to update reference image meta",{type:"snackbar"})}},help:"Add to the list of reference images."}))))}),(0,k.addFilter)("blocks.registerBlockType","kaigen/add-reference-image-attribute",(e,t)=>"core/image"!==t?e:{...e,attributes:{...e.attributes,kaigen_reference_image:{type:"boolean",default:!1}}})})();1 (()=>{"use strict";var e={n:t=>{var a=t&&t.__esModule?()=>t.default:()=>t;return e.d(a,{a}),a},d:(t,a)=>{for(var n in a)e.o(a,n)&&!e.o(t,n)&&Object.defineProperty(t,n,{enumerable:!0,get:a[n]})},o:(e,t)=>Object.prototype.hasOwnProperty.call(e,t)};const t=window.React,a=window.wp.element,n=window.wp.components,r=async(e,t,a={})=>{try{const n=wp.data.select("core/editor")?.getEditorSettings()?.kaigen_provider;if(!n)throw new Error("No provider configured. Please check your plugin settings.");const r=wp.data.select("core/editor")?.getEditorSettings()?.kaigen_has_api_key;if(!r)throw new Error("No API key configured for the selected provider. Please add one in the KaiGen settings.");const l={prompt:e,provider:n};a.sourceImageUrls&&Array.isArray(a.sourceImageUrls)?l.source_image_urls=a.sourceImageUrls:a.sourceImageUrl&&(l.source_image_url=a.sourceImageUrl),a.sourceImageIds&&Array.isArray(a.sourceImageIds)&&(l.source_image_ids=a.sourceImageIds),a.additionalImageUrls&&Array.isArray(a.additionalImageUrls)&&(l.additional_image_urls=a.additionalImageUrls),a.maskUrl&&(l.mask_url=a.maskUrl),a.moderation&&["auto","low"].includes(a.moderation)&&(l.moderation=a.moderation),a.style&&["natural","vivid"].includes(a.style)&&(l.style=a.style),a.aspectRatio&&["1:1","16:9","9:16","4:3","3:4"].includes(a.aspectRatio)&&(l.aspect_ratio=a.aspectRatio),a.quality&&["low","medium","high"].includes(a.quality)&&(l.quality=a.quality);const i=wp.apiFetch({path:"/kaigen/v1/estimated-generation-time",method:"POST",data:l});"function"==typeof a.onEstimatedTime&&i.then(e=>{e&&"number"==typeof e.estimated_time_seconds&&a.onEstimatedTime(e.estimated_time_seconds)}).catch(()=>{});const o=await wp.apiFetch({path:"/kaigen/v1/generate-image",method:"POST",data:l});if(o.code&&o.message){if("content_moderation"===o.code)throw new Error(o.message);if("replicate_error"===o.code)throw new Error("Image generation failed: "+o.message);throw new Error(o.message)}if(!o||!o.url)throw new Error("Invalid response from server: "+JSON.stringify(o));o.id&&"number"==typeof o.id&&o.id>0?t({url:o.url,alt:e,id:o.id,caption:""}):t({url:o.url,alt:e,caption:""})}catch(e){t({error:e.message||"An unknown error occurred while generating the image"})}},l=(e,t)=>{const[n,r]=(0,a.useState)(0),l=(0,a.useRef)(3e4),i=(0,a.useRef)(null),o=(0,a.useRef)(0);return(0,a.useEffect)(()=>{l.current="number"==typeof t&&t>0?t:3e4},[t]),(0,a.useEffect)(()=>{if(!e)return r(0),i.current=null,void(o.current=0);i.current||(i.current=Date.now(),r(0),o.current=0);const t=setInterval(()=>{const e=Date.now()-i.current,t=Math.min(Math.floor(e/l.current*100),99),a=Math.max(t,o.current);o.current=a,r(a)},200);return()=>clearInterval(t)},[e]),n},i=window.kaiGen?.logoUrl,o=({isOpen:e,onClose:o,onSelect:c,initialReferenceImage:s})=>{const[m,d]=(0,a.useState)(""),[u,g]=(0,a.useState)(!1),[p,k]=(0,a.useState)(null),[E,b]=(0,a.useState)([]),[h,f]=(0,a.useState)([]),[_,w]=(0,a.useState)("1:1"),[y,v]=(0,a.useState)("medium"),[N,I]=(0,a.useState)(null),S=wp.data.select("core/editor")?.getEditorSettings()||{},G=S.kaigen_provider||"replicate",C=S.kaigen_quality||"medium",T="replicate"===G?10:16,x=l(u,N);(0,a.useEffect)(()=>{e&&((async()=>{try{const e=await wp.apiFetch({path:"/kaigen/v1/reference-images",method:"GET"});return Array.isArray(e)?e:[]}catch(e){return[]}})().then(b),v(C),s&&s.url?f([s]):f([]))},[e,s,C]);const A=()=>{if(!m.trim())return void k("Please enter a prompt for image generation.");g(!0),I(null),k(null);const e={};h.length>0&&(e.sourceImageUrls=h.map(e=>e.url),e.sourceImageIds=h.map(e=>e.id).filter(e=>Number.isInteger(e)&&e>0)),_&&(e.aspectRatio=_),y&&(e.quality=y),e.onEstimatedTime=e=>{"number"==typeof e&&I(1e3*e)},r(m.trim(),e=>{e.error?(k(e.error),g(!1)):(c(e),g(!1),B())},e)},B=()=>{d(""),k(null),f([]),v(C),I(null),o()};return e?(0,t.createElement)(n.Modal,{className:"kaigen-modal",title:(0,t.createElement)("div",{className:"kaigen-modal__logo-container"},(0,t.createElement)("img",{src:i,alt:"KaiGen logo",className:"kaigen-modal__logo"})),"aria-label":"KaiGen",onRequestClose:B},p&&(0,t.createElement)("p",{className:"kaigen-error-text"},p),(0,t.createElement)("div",{className:"kaigen-modal__input-container"},(0,t.createElement)(n.Dropdown,{popoverProps:{placement:"bottom-start",focusOnMount:!0},renderToggle:({isOpen:e,onToggle:a})=>(0,t.createElement)(n.Button,{className:"kaigen-modal__ref-button "+(h.length>0?"kaigen-ref-button-selected":""),onClick:a,"aria-expanded":e,"aria-label":"Reference Images"},(0,t.createElement)(n.Dashicon,{icon:"format-image",className:h.length>0?"kaigen-ref-button-icon-selected":""})),renderContent:()=>{const e=s?[s,...E.filter(e=>e.id!==s.id)]:E;return(0,t.createElement)("div",{className:"kaigen-modal-dropdown-content-container"},(0,t.createElement)("h4",{className:"kaigen-modal-dropdown-content-title"},"Reference Images (up to ",T,")"),e.length>0?(0,t.createElement)("div",{className:"kaigen-modal-reference-images-container"},e.map((e,a)=>(0,t.createElement)("button",{type:"button",key:e.id||`initial-${a}`,onClick:()=>{f(t=>t.some(t=>t.url===e.url)?t.filter(t=>t.url!==e.url):t.length<T?[...t,e]:t)},className:"kaigen-modal-reference-image "+(h.some(t=>t.url===e.url)?"kaigen-modal-reference-image-selected":""),"aria-label":e.alt||"Select reference image"},(0,t.createElement)("img",{src:e.thumbnail_url||e.url,alt:e.alt||""})))):(0,t.createElement)("p",{className:"kaigen-modal-no-references"},"No reference images. Mark images in the Media Library to use them here."))}}),(0,t.createElement)("div",{className:"kaigen-modal__textarea-container"},(0,t.createElement)(n.TextareaControl,{className:"kaigen-modal__textarea",placeholder:"Image prompt...",value:m,onChange:d,onKeyDown:e=>{"Enter"!==e.key||e.shiftKey||(e.preventDefault(),A())},rows:2})),m.trim()&&(0,t.createElement)(n.Button,{className:"kaigen-modal__submit-button",variant:"primary",onClick:A,disabled:u||!m.trim(),"aria-label":"Generate Image"},(0,t.createElement)(n.Dashicon,{icon:"admin-appearance"})),(0,t.createElement)(n.Dropdown,{popoverProps:{placement:"bottom-end",focusOnMount:!0},renderToggle:({isOpen:e,onToggle:a})=>(0,t.createElement)(n.Button,{className:"kaigen-modal__settings-button",onClick:a,"aria-expanded":e,"aria-label":"Settings"},(0,t.createElement)(n.Dashicon,{icon:"admin-generic"})),renderContent:()=>(0,t.createElement)("div",{className:"kaigen-modal-dropdown-content-container"},(0,t.createElement)("h4",{className:"kaigen-modal-dropdown-content-title"},"Aspect Ratio"),(0,t.createElement)("div",{className:"kaigen-modal-aspect-ratio-container"},[{value:"1:1",label:"1:1",title:"Square"},{value:"16:9",label:"16:9",title:"Landscape"},{value:"9:16",label:"9:16",title:"Portrait"}].map(e=>(0,t.createElement)("button",{type:"button",key:e.value,onClick:()=>w(t=>t===e.value?null:e.value),"aria-pressed":_===e.value,"aria-label":`${e.title} (${e.label})`,className:"kaigen-modal__aspect-ratio-button "+(_===e.value?"kaigen-modal__aspect-ratio-button-selected":"")},(0,t.createElement)("div",{className:"kaigen-modal-aspect-ratio-icon-container"},(0,t.createElement)("div",{className:`kaigen-modal-aspect-ratio-icon ${_===e.value?"kaigen-modal-aspect-ratio-icon-selected":""} kaigen-aspect-ratio-${e.value.replace(":","-")}`})),(0,t.createElement)("span",{className:"kaigen-modal-aspect-ratio-label"},e.label)))),(0,t.createElement)("h4",{className:"kaigen-modal-dropdown-content-title"},"Quality"),(0,t.createElement)("div",{className:"kaigen-modal-quality-container"},[{value:"low",label:"Low"},{value:"medium",label:"Medium"},{value:"high",label:"High"}].map(e=>(0,t.createElement)("button",{type:"button",key:e.value,onClick:()=>v(e.value),"aria-pressed":y===e.value,className:"kaigen-modal__quality-button "+(y===e.value?"kaigen-modal__quality-button-selected":"")},e.label))))})),u&&(0,t.createElement)("div",{className:"kaigen-modal__progress"},(0,t.createElement)("div",{className:"kaigen-modal__progress-label"},"Generating... ",x,"%"),(0,t.createElement)("div",{className:"kaigen-modal__progress-track",role:"progressbar","aria-valuenow":x,"aria-valuemin":0,"aria-valuemax":100,"aria-label":"Image generation progress"},(0,t.createElement)("div",{className:"kaigen-modal__progress-fill",style:{width:`${x}%`}})))):null},c=window.kaiGen?.logoUrl,s=({onSelect:e,shouldDisplay:r})=>{const[l,i]=(0,a.useState)(!1);return r?(0,t.createElement)(t.Fragment,null,(0,t.createElement)(n.Button,{onClick:()=>i(!0),className:"kaigen-placeholder-button","aria-label":"KaiGen"},(0,t.createElement)("img",{src:c,alt:"KaiGen",style:{width:"48px",height:"48px"}})),(0,t.createElement)("button",{type:"button",role:"menuitem",onClick:()=>i(!0),className:"components-button components-menu-item__button is-next-40px-default-size kaigen-menu-item-button"},(0,t.createElement)("span",{className:"components-menu-item__item"},"KaiGen"),(0,t.createElement)("img",{src:c,alt:"","aria-hidden":"true",className:"components-menu-items__item-icon has-icon-right",style:{width:"24px",height:"24px"}})),(0,t.createElement)(o,{isOpen:l,onClose:()=>i(!1),onSelect:e})):null},m=window.kaiGen?.logoUrl,d=({isGenerating:e,onGenerateImage:r,isRegenerating:i,onImageGenerated:c,isImageBlock:s,isTextSelected:d,currentImage:u,estimatedDurationMs:g})=>{const[p,k]=(0,a.useState)(!1),E=l(e||i,g),b=(0,t.createElement)("span",{className:"kaigen-progress-icon","aria-hidden":"true"},(0,t.createElement)("span",{className:"kaigen-progress-icon__track"},(0,t.createElement)("span",{className:"kaigen-progress-icon__fill",style:{width:`${E}%`}})));return s?(0,t.createElement)(t.Fragment,null,(0,t.createElement)(n.ToolbarGroup,null,(0,t.createElement)(n.ToolbarButton,{icon:i?b:(0,t.createElement)("img",{src:m,alt:"KaiGen logo",className:"kaigen-toolbar-icon"}),label:i?`KaiGen is generating... ${E}%`:"KaiGen",onClick:()=>k(!0),disabled:i})),(0,t.createElement)(o,{isOpen:p,onClose:()=>k(!1),onSelect:c,initialReferenceImage:u})):d?(0,t.createElement)(n.ToolbarGroup,null,(0,t.createElement)(n.ToolbarButton,{icon:e?b:"format-image",label:e?`KaiGen is generating... ${E}%`:"KaiGen",onClick:r,disabled:e})):null},u=window.wp.blockEditor,g=window.wp.data,p=window.wp.richText,k=({value:e})=>{const[n,l]=(0,a.useState)(!1),[i,o]=(0,a.useState)(null),c=(0,g.useSelect)(e=>e("core/block-editor").getSelectedBlock(),[]),{replaceBlocks:s}=(0,g.useDispatch)("core/block-editor"),m=(0,a.useCallback)(()=>{if(c&&"core/paragraph"===c.name){const t=e.text.slice(e.start,e.end).trim();if(!t)return void wp.data.dispatch("core/notices").createErrorNotice("Please select some text to use as the image generation prompt.",{type:"snackbar"});const a=wp.data.select("core/editor")?.getEditorSettings()?.kaigen_provider;if(!a)return void wp.data.dispatch("core/notices").createErrorNotice("No AI provider configured. Please set one in the plugin settings.",{type:"snackbar"});const n=wp.blocks.createBlock("core/heading",{content:"Generating AI image...",level:2,className:"kaigen-text-center"});s(c.clientId,[n,c]),l(!0),o(null),r(t,e=>{if(l(!1),o(null),e.error)wp.data.dispatch("core/notices").createErrorNotice("Failed to generate image: "+e.error,{type:"snackbar"}),s(n.clientId,[]);else{const t={url:e.url,alt:e.alt,caption:""};e.id&&"number"==typeof e.id&&e.id>0&&(t.id=e.id);const a=wp.blocks.createBlock("core/image",t);s(n.clientId,[a])}},{onEstimatedTime:e=>{"number"==typeof e&&o(1e3*e)}})}},[c,e.text,e.start,e.end,s]),p=""!==e.text.slice(e.start,e.end).trim();return(0,t.createElement)(u.BlockControls,null,(0,t.createElement)(d,{isGenerating:n,onGenerateImage:m,isTextSelected:p,estimatedDurationMs:i}))};(0,p.registerFormatType)("kaigen/custom-format",{title:"AI Image Gen",tagName:"span",className:"kaigen-format",edit:({value:e})=>(0,t.createElement)(k,{value:e})});const E=window.wp.hooks;(0,E.addFilter)("editor.MediaUpload","kaigen/add-ai-tab",e=>a=>{const n=a.allowedTypes&&a.allowedTypes.includes("image")&&!a.multiple,r=wp.data.select("core/block-editor").getSelectedBlock(),l=r&&"core/image"===r.name,i=wp.data.select("core/editor")?.getEditorSettings()?.kaigen_has_api_key,o=n&&l&&!(r&&r.attributes&&r.attributes.url)&&i;return(0,t.createElement)(e,{...a,render:e=>(0,t.createElement)(t.Fragment,null,a.render(e),(0,t.createElement)(s,{onSelect:a.onSelect,shouldDisplay:o}))})});const b=window.wp.apiFetch;var h=e.n(b);(0,E.addFilter)("editor.BlockEdit","kaigen/add-regenerate-button",e=>r=>{if("core/image"!==r.name)return(0,t.createElement)(e,{...r});const l=r.attributes.id&&"number"==typeof r.attributes.id&&r.attributes.id>0,[i,o]=(0,a.useState)(!1),[c,s]=(0,a.useState)(null),[m,g]=(0,a.useState)(!1),[p,k]=(0,a.useState)([]),[E,b]=(0,a.useState)(!1),[f,_]=(0,a.useState)(null),{attributes:{id:w,kaigen_reference_image:y},setAttributes:v}=r;(0,a.useEffect)(()=>{l&&!i&&(null==y?h()({path:`/wp/v2/media/${w}`}).then(e=>{if(e&&e.meta&&void 0!==e.meta.kaigen_reference_image){const t=!0===e.meta.kaigen_reference_image||1===e.meta.kaigen_reference_image;v({kaigen_reference_image:t})}o(!0)}).catch(()=>{o(!0)}):o(!0))},[l,w,y,i,v]),(0,a.useEffect)(()=>{w&&(s(null),k([]),_(null))},[w]),(0,a.useEffect)(()=>{E&&w&&f!==w&&(g(!0),h()({path:`/kaigen/v1/generation-meta?attachment_id=${w}`}).then(e=>{s(e&&Object.keys(e).length?e:null)}).catch(()=>{s(null)}).finally(()=>{g(!1),_(w)}))},[E,w,f]),(0,a.useEffect)(()=>{if(!(E&&c&&Array.isArray(c.reference_image_ids)&&c.reference_image_ids.length))return void k([]);const e=c.reference_image_ids.join(",");h()({path:`/wp/v2/media?include=${e}&per_page=${c.reference_image_ids.length}`}).then(e=>{const t=Array.isArray(e)?e.map(e=>({id:e.id,url:e.media_details?.sizes?.thumbnail?.source_url||e.source_url})).filter(e=>e.url):[];k(t)}).catch(()=>{k([])})},[E,c]);const N=r.attributes.url?{url:r.attributes.url,id:r.attributes.id,alt:r.attributes.alt||""}:null;return(0,t.createElement)(t.Fragment,null,(0,t.createElement)(e,{...r}),r.attributes.url&&(0,t.createElement)(u.BlockControls,null,(0,t.createElement)(d,{onImageGenerated:e=>{e.id&&"number"==typeof e.id&&e.id>0?r.setAttributes({url:e.url,id:e.id}):r.setAttributes({url:e.url,id:void 0}),wp.data.dispatch("core/notices").createSuccessNotice("Image generated successfully!",{type:"snackbar"})},isImageBlock:!0,currentImage:N})),l&&(0,t.createElement)(u.InspectorControls,null,(0,t.createElement)(n.PanelBody,{title:"KaiGen Settings",initialOpen:!1,onToggle:e=>{b(e)}},(0,t.createElement)(n.CheckboxControl,{label:"Reference image",checked:!0===r.attributes.kaigen_reference_image,onChange:async e=>{const t=!0===e;r.setAttributes({kaigen_reference_image:t}),o(!0);try{await h()({path:`/wp/v2/media/${r.attributes.id}`,method:"POST",data:{meta:{kaigen_reference_image:t?1:0}}})}catch(e){wp.data.dispatch("core/notices").createErrorNotice("Failed to update reference image meta",{type:"snackbar"})}},help:"Add to the list of reference images."}),m&&(0,t.createElement)("p",{className:"kaigen-generation-meta-loading"},"Loading generation details..."),!m&&c&&(0,t.createElement)("table",{className:"kaigen-generation-meta-table"},(0,t.createElement)("tbody",null,(0,t.createElement)("tr",null,(0,t.createElement)("th",null,"Prompt"),(0,t.createElement)("td",null,c.prompt)),(0,t.createElement)("tr",null,(0,t.createElement)("th",null,"Provider"),(0,t.createElement)("td",null,c.provider)),(0,t.createElement)("tr",null,(0,t.createElement)("th",null,"Quality"),(0,t.createElement)("td",null,c.quality)),(0,t.createElement)("tr",null,(0,t.createElement)("th",null,"Model"),(0,t.createElement)("td",null,c.model)),p.length>0&&(0,t.createElement)("tr",null,(0,t.createElement)("th",null,"References"),(0,t.createElement)("td",null,(0,t.createElement)("div",{className:"kaigen-generation-meta-images"},p.map(e=>(0,t.createElement)("img",{key:e.id,src:e.url,alt:"",className:"kaigen-generation-meta-image"}))))))))))}),(0,E.addFilter)("blocks.registerBlockType","kaigen/add-reference-image-attribute",(e,t)=>"core/image"!==t?e:{...e,attributes:{...e.attributes,kaigen_reference_image:{type:"boolean",default:!1}}})})(); -
kaigen/trunk/inc/class-admin.php
r3424397 r3428748 94 94 } 95 95 96 $quality = Image_Provider::get_quality_setting(); 97 $provider_models = get_option( 'kaigen_provider_models', [] ); 98 $provider_model = $provider_models[ $provider ] ?? ''; 99 $estimated_time = 30; 100 101 $provider_instance = kaigen_provider_manager()->get_provider( $provider ); 102 if ( $provider_instance ) { 103 if ( ! empty( $provider_model ) ) { 104 $provider_instance->set_model( $provider_model ); 105 } 106 $estimated_time = (int) $provider_instance->get_estimated_generation_time( $quality, [] ); 107 } 108 96 109 // Add the provider setting in all possible locations to ensure it's available. 97 $settings['kaigen_provider'] = $provider; 110 $settings['kaigen_provider'] = $provider; 111 $settings['kaigen_quality'] = $quality; 112 $settings['kaigen_provider_model'] = $provider_model; 113 $settings['kaigen_estimated_generation_time_seconds'] = $estimated_time; 98 114 99 115 if ( ! isset( $settings['kaigen'] ) ) { 100 116 $settings['kaigen'] = []; 101 117 } 102 $settings['kaigen']['provider'] = $provider; 118 $settings['kaigen']['provider'] = $provider; 119 $settings['kaigen']['quality'] = $quality; 120 $settings['kaigen']['provider_model'] = $provider_model; 121 $settings['kaigen']['estimated_generation_time_seconds'] = $estimated_time; 103 122 104 123 // Add to editor settings directly. … … 106 125 $settings['kaigen_settings'] = []; 107 126 } 108 $settings['kaigen_settings']['provider'] = $provider; 109 $settings['kaigen_has_api_key'] = ! empty( $provider ) && ! empty( $api_keys[ $provider ] ); 127 $settings['kaigen_settings']['provider'] = $provider; 128 $settings['kaigen_settings']['quality'] = $quality; 129 $settings['kaigen_settings']['provider_model'] = $provider_model; 130 $settings['kaigen_settings']['estimated_generation_time_seconds'] = $estimated_time; 131 $settings['kaigen_has_api_key'] = ! empty( $provider ) && ! empty( $api_keys[ $provider ] ); 110 132 111 133 return $settings; -
kaigen/trunk/inc/class-image-provider.php
r3424397 r3428748 60 60 } 61 61 return false; 62 } 63 64 /** 65 * Gets the estimated image generation time in seconds. 66 * 67 * @param string $quality_setting Optional quality setting. 68 * @param array $additional_params Optional additional parameters for estimation. 69 * @return int Estimated time in seconds. 70 */ 71 public function get_estimated_generation_time( $quality_setting = '', $additional_params = [] ) { 72 return 30; 73 } 74 75 /** 76 * Resolves the model to use for a request. 77 * 78 * @param string $quality_setting Optional quality setting. 79 * @param array $additional_params Optional additional parameters for the request. 80 * @return string The resolved model identifier. 81 */ 82 public function get_model_for_request( $quality_setting = '', $additional_params = [] ) { 83 return $this->model; 62 84 } 63 85 -
kaigen/trunk/inc/class-rest-api.php
r3424397 r3428748 7 7 8 8 namespace KaiGen; 9 10 use WP_Error; 9 11 10 12 /** … … 99 101 ] 100 102 ); 103 104 // Register the estimated generation time endpoint. 105 register_rest_route( 106 self::API_NAMESPACE, 107 '/estimated-generation-time', 108 [ 109 'methods' => 'POST', 110 'callback' => [ $this, 'get_estimated_generation_time' ], 111 'permission_callback' => [ $this, 'check_permission' ], 112 ] 113 ); 114 115 // Register the generation metadata endpoint. 116 register_rest_route( 117 self::API_NAMESPACE, 118 '/generation-meta', 119 [ 120 'methods' => 'GET', 121 'callback' => [ $this, 'get_generation_meta' ], 122 'permission_callback' => [ $this, 'check_permission' ], 123 ] 124 ); 101 125 } 102 126 … … 121 145 $provider_id = $request->get_param( 'provider' ); 122 146 147 // Get additional parameters with defaults. 148 $additional_params = $this->get_additional_params( $request ); 149 123 150 // Get provider model. 124 $model = $this->get_provider_model( $provider_id );151 $model = $this->get_provider_model( $provider_id, $additional_params ); 125 152 if ( is_wp_error( $model ) ) { 126 153 return $model; 127 154 } 128 155 129 // Get additional parameters with defaults. 156 // Handle retries for image generation. 157 $response = $this->handle_generation_with_retries( $provider_id, $prompt, $model, $additional_params ); 158 159 if ( $response instanceof \WP_REST_Response ) { 160 $response_data = $response->get_data(); 161 if ( ! empty( $response_data['id'] ) ) { 162 $this->maybe_save_generation_meta( 163 absint( $response_data['id'] ), 164 $request, 165 $provider_id, 166 $model 167 ); 168 } 169 } 170 171 return $response; 172 } 173 174 /** 175 * Gets the estimated generation time for a request. 176 * 177 * @param WP_REST_Request $request The request object. 178 * @return WP_REST_Response|WP_Error The response or error. 179 */ 180 public function get_estimated_generation_time( $request ) { 181 $provider_id = $request->get_param( 'provider' ); 182 if ( empty( $provider_id ) ) { 183 return new WP_Error( 'invalid_provider', 'Provider is required.', [ 'status' => 400 ] ); 184 } 185 130 186 $additional_params = $this->get_additional_params( $request ); 131 132 // Handle retries for image generation. 133 return $this->handle_generation_with_retries( $provider_id, $prompt, $model, $additional_params ); 187 $model = $this->get_provider_model( $provider_id, $additional_params ); 188 if ( is_wp_error( $model ) ) { 189 return $model; 190 } 191 $quality = $additional_params['quality'] ?? Image_Provider::get_quality_setting(); 192 $provider = kaigen_provider_manager()->get_provider( $provider_id ); 193 194 if ( ! $provider ) { 195 return new WP_Error( 'invalid_provider', "Invalid provider: {$provider_id}", [ 'status' => 400 ] ); 196 } 197 198 $api_keys = get_option( 'kaigen_provider_api_keys', [] ); 199 $api_key = isset( $api_keys[ $provider_id ] ) ? $api_keys[ $provider_id ] : ''; 200 201 $provider_class = get_class( $provider ); 202 $provider_instance = new $provider_class( $api_key, $model ); 203 $estimated_time = (int) $provider_instance->get_estimated_generation_time( $quality, $additional_params ); 204 205 return new \WP_REST_Response( 206 [ 'estimated_time_seconds' => $estimated_time ], 207 200 208 ); 209 } 210 211 /** 212 * Gets the stored generation metadata for a post. 213 * 214 * @param WP_REST_Request $request The request object. 215 * @return WP_REST_Response|WP_Error The response or error. 216 */ 217 public function get_generation_meta( $request ) { 218 $attachment_id = absint( $request->get_param( 'attachment_id' ) ); 219 if ( ! $attachment_id ) { 220 return new WP_Error( 'invalid_attachment_id', 'Attachment ID is required.', [ 'status' => 400 ] ); 221 } 222 223 if ( ! current_user_can( 'edit_post', $attachment_id ) ) { 224 return new WP_Error( 'forbidden', 'You do not have permission to view this attachment.', [ 'status' => 403 ] ); 225 } 226 227 $meta = get_post_meta( $attachment_id, 'kaigen_generation_meta', true ); 228 if ( ! is_array( $meta ) ) { 229 $meta = []; 230 } 231 232 return new \WP_REST_Response( $meta, 200 ); 233 } 234 235 /** 236 * Saves generation metadata on the post when available. 237 * 238 * @param int $attachment_id The attachment ID. 239 * @param WP_REST_Request $request The request object. 240 * @param string $provider_id The provider ID. 241 * @param string $model The resolved model. 242 * @return void 243 */ 244 private function maybe_save_generation_meta( $attachment_id, $request, $provider_id, $model ) { 245 if ( ! $attachment_id ) { 246 return; 247 } 248 249 if ( ! current_user_can( 'edit_post', $attachment_id ) ) { 250 return; 251 } 252 253 $quality = $request->get_param( 'quality' ); 254 if ( ! in_array( $quality, [ 'low', 'medium', 'high' ], true ) ) { 255 $quality = Image_Provider::get_quality_setting(); 256 } 257 $meta = [ 258 'prompt' => sanitize_text_field( (string) $request->get_param( 'prompt' ) ), 259 'provider' => sanitize_text_field( $provider_id ), 260 'quality' => sanitize_text_field( $quality ), 261 'model' => sanitize_text_field( $model ), 262 ]; 263 $source_image_ids = $request->get_param( 'source_image_ids' ); 264 if ( is_array( $source_image_ids ) ) { 265 $sanitized_ids = array_values( 266 array_filter( 267 array_map( 'absint', $source_image_ids ) 268 ) 269 ); 270 if ( ! empty( $sanitized_ids ) ) { 271 $meta['reference_image_ids'] = $sanitized_ids; 272 } 273 } 274 275 update_post_meta( $attachment_id, 'kaigen_generation_meta', $meta ); 134 276 } 135 277 … … 138 280 * 139 281 * @param string $provider_id The provider ID. 282 * @param array $additional_params Additional parameters. 140 283 * @return string|WP_Error The model or error. 141 284 */ 142 private function get_provider_model( $provider_id ) { 143 // For Replicate, get the model based on quality setting. 144 if ( 'replicate' === $provider_id ) { 145 $quality = Image_Provider::get_quality_setting(); 146 147 $provider = kaigen_provider_manager()->get_provider( $provider_id ); 148 if ( $provider ) { 149 $model = $provider->get_model_from_quality_setting( $quality ); 150 return $model; 151 } 152 } 153 154 // For other providers, use the stored model or default. 285 private function get_provider_model( $provider_id, $additional_params = [] ) { 286 $provider = kaigen_provider_manager()->get_provider( $provider_id ); 287 if ( ! $provider ) { 288 return new WP_Error( 'invalid_provider', "Invalid provider: {$provider_id}", [ 'status' => 400 ] ); 289 } 290 155 291 $provider_models = get_option( 'kaigen_provider_models', [] ); 156 $default_models = [ 157 'openai' => 'dall-e-3', 158 ]; 159 160 if ( ! empty( $provider_models[ $provider_id ] ) ) { 161 return $provider_models[ $provider_id ]; 162 } 163 164 if ( ! empty( $default_models[ $provider_id ] ) ) { 165 $model = $default_models[ $provider_id ]; 166 $provider_models[ $provider_id ] = $model; 167 update_option( 'kaigen_provider_models', $provider_models ); 168 return $model; 169 } 170 171 return new \WP_Error( 'model_not_set', "No model set for provider: {$provider_id}", [ 'status' => 400 ] ); 292 $stored_model = $provider_models[ $provider_id ] ?? ''; 293 $quality = $additional_params['quality'] ?? Image_Provider::get_quality_setting(); 294 295 $api_keys = get_option( 'kaigen_provider_api_keys', [] ); 296 $api_key = isset( $api_keys[ $provider_id ] ) ? $api_keys[ $provider_id ] : ''; 297 298 $provider_class = get_class( $provider ); 299 $provider_instance = new $provider_class( $api_key, $stored_model ); 300 $model = $provider_instance->get_model_for_request( $quality, $additional_params ); 301 302 if ( empty( $model ) ) { 303 return new WP_Error( 'model_not_set', "No model set for provider: {$provider_id}", [ 'status' => 400 ] ); 304 } 305 306 return $model; 172 307 } 173 308 … … 184 319 $quality_settings = get_option( 'kaigen_quality_settings', [] ); 185 320 $style_value = isset( $quality_settings['style'] ) ? $quality_settings['style'] : 'natural'; 321 $quality_override = $request->get_param( 'quality' ); 322 if ( in_array( $quality_override, [ 'low', 'medium', 'high' ], true ) ) { 323 $quality = $quality_override; 324 } 186 325 187 326 $defaults = [ … … 201 340 $params[ $key ] = $request->get_param( $key ) ?? $default; 202 341 } 342 $params['quality'] = $quality; 203 343 204 344 // Add source image URL if provided (single or array). … … 276 416 if ( isset( $result['status'] ) && 'failed' === $result['status'] ) { 277 417 if ( isset( $result['error'] ) && strpos( $result['error'], 'flagged by safety filters' ) !== false ) { 278 return new \WP_Error(418 return new WP_Error( 279 419 'content_filtered', 280 420 'The image was flagged by the provider\'s safety filters. Please modify your prompt and try again.', … … 357 497 358 498 if ( $retry_count >= $max_retries ) { 359 return new \WP_Error(499 return new WP_Error( 360 500 'api_error', 361 501 'Failed after ' . $max_retries . ' attempts: ' . $e->getMessage(), … … 382 522 $provider = kaigen_provider_manager()->get_provider( $provider_id ); 383 523 if ( ! $provider ) { 384 return new \WP_Error( 'invalid_provider', "Invalid provider: {$provider_id}" );524 return new WP_Error( 'invalid_provider', "Invalid provider: {$provider_id}" ); 385 525 } 386 526 -
kaigen/trunk/inc/interface-image-provider.php
r3424397 r3428748 85 85 */ 86 86 public function set_model( $model ); 87 88 /** 89 * Gets the estimated image generation time in seconds. 90 * 91 * @param string $quality_setting Optional quality setting. 92 * @param array $additional_params Optional additional parameters for estimation. 93 * @return int Estimated time in seconds. 94 */ 95 public function get_estimated_generation_time( $quality_setting = '', $additional_params = [] ); 96 97 /** 98 * Resolves the model to use for a request. 99 * 100 * @param string $quality_setting Optional quality setting. 101 * @param array $additional_params Optional additional parameters for the request. 102 * @return string The resolved model identifier. 103 */ 104 public function get_model_for_request( $quality_setting = '', $additional_params = [] ); 87 105 } -
kaigen/trunk/inc/providers/class-image-provider-openai.php
r3424397 r3428748 9 9 10 10 use KaiGen\Image_Provider; 11 use WP_Error; 11 12 12 13 /** … … 73 74 74 75 $max_retries = 3; 75 $timeout = 150; // Increased timeout for image generation (docs say up to 2 mins).76 $timeout = 360; // Allow long-running high-quality generations. 76 77 $retry_delay = 2; // Seconds to wait between retries. 77 78 // Add filter to ensure WordPress respects our timeout settings.79 add_filter(80 'http_request_timeout',81 function () use ( $timeout ) {82 return $timeout;83 }84 );85 86 // Add filter to set cURL options.87 add_filter(88 'http_request_args',89 function ( $args ) use ( $timeout ) {90 $args['timeout'] = $timeout;91 $args['sslverify'] = true;92 $args['blocking'] = true;93 94 // Set cURL options directly.95 if ( ! isset( $args['curl'] ) ) {96 $args['curl'] = [];97 }98 $args['curl'][ CURLOPT_TIMEOUT ] = $timeout;99 $args['curl'][ CURLOPT_CONNECTTIMEOUT ] = 30; // Increased connect timeout.100 $args['curl'][ CURLOPT_TCP_KEEPALIVE ] = 1; // Enable TCP keepalive.101 102 // Adjust low speed settings to prevent timeouts on slow generation.103 $args['curl'][ CURLOPT_LOW_SPEED_TIME ] = 600; // Wait 10 minutes before timing out due to low speed.104 $args['curl'][ CURLOPT_LOW_SPEED_LIMIT ] = 1; // Only timeout if speed is effectively 0.105 106 return $args;107 }108 );109 78 110 79 // Default to API_BASE_URL. … … 116 85 } 117 86 118 // Get quality setting from admin options. 119 $quality = self::get_quality_setting(); 87 // Get quality setting from admin options or request override. 88 $quality = $additional_params['quality'] ?? self::get_quality_setting(); 89 90 // Scale timeout based on quality to keep faster requests snappy. 91 $timeout = 180; 92 if ( 'low' === $quality ) { 93 $timeout = 90; 94 } elseif ( 'high' === $quality ) { 95 $timeout = 360; 96 } 120 97 121 98 // Map quality settings to supported values. … … 156 133 $body .= $quality . "\r\n"; 157 134 135 // Add moderation parameter. 136 $body .= "--{$boundary}\r\n"; 137 $body .= 'Content-Disposition: form-data; name="moderation"' . "\r\n\r\n"; 138 $body .= "low\r\n"; 139 158 140 // Add format parameter (jpeg is faster than png). 159 141 $body .= "--{$boundary}\r\n"; … … 184 166 'prompt' => $prompt, 185 167 'quality' => $quality, 168 'moderation' => 'low', 186 169 'output_format' => 'jpeg', 187 170 ]; … … 197 180 198 181 // Make the API request with retries. 199 $attempt = 0; 200 $last_error = null; 182 $attempt = 0; 183 $last_error = null; 184 $final_response = null; 185 $final_error = null; 186 187 $curl_override = function ( $handle, $request_args, $url ) use ( $timeout ) { 188 if ( false === strpos( $url, 'api.openai.com' ) ) { 189 return; 190 } 191 192 // phpcs:ignore WordPress.WP.AlternativeFunctions.curl_curl_setopt -- Needed to prevent low-speed aborts for long-running requests. 193 curl_setopt( $handle, CURLOPT_TIMEOUT, $timeout ); 194 // phpcs:ignore WordPress.WP.AlternativeFunctions.curl_curl_setopt -- Needed to prevent low-speed aborts for long-running requests. 195 curl_setopt( $handle, CURLOPT_CONNECTTIMEOUT, 30 ); 196 // phpcs:ignore WordPress.WP.AlternativeFunctions.curl_curl_setopt -- Needed to prevent low-speed aborts for long-running requests. 197 curl_setopt( $handle, CURLOPT_LOW_SPEED_TIME, 180 ); 198 // phpcs:ignore WordPress.WP.AlternativeFunctions.curl_curl_setopt -- Needed to prevent low-speed aborts for long-running requests. 199 curl_setopt( $handle, CURLOPT_LOW_SPEED_LIMIT, 1 ); 200 }; 201 202 add_action( 'http_api_curl', $curl_override, 10, 3 ); 201 203 202 204 while ( $attempt < $max_retries ) { … … 224 226 } 225 227 226 // For other errors, return immediately.227 return $response;228 $final_error = new WP_Error( 'openai_error', 'OpenAI API request failed: ' . $error_message ); 229 break; 228 230 } 229 231 … … 232 234 233 235 if ( 200 !== $response_code ) { 234 235 236 // Parse error response. 236 237 $error_data = json_decode( $response_body, true ); … … 240 241 // Check for specific error about image URL in prompt. 241 242 if ( strpos( $error_message, 'image URL' ) !== false ) { 242 243 243 // Remove the image URL from the body and retry. 244 244 $body['prompt'] = $prompt; … … 257 257 258 258 if ( $retry_code < 400 ) { 259 return json_decode( $retry_body, true ); 259 $final_response = json_decode( $retry_body, true ); 260 break; 260 261 } 261 262 } 262 263 } 263 264 264 return new WP_Error( 'openai_error', $error_message ); 265 $final_error = new WP_Error( 'openai_error', $error_message ); 266 break; 265 267 } 266 268 267 return new WP_Error( 'api_error', "API Error (HTTP $response_code): $response_body" ); 269 $final_error = new WP_Error( 'api_error', "API Error (HTTP $response_code): $response_body" ); 270 break; 268 271 } 269 272 270 273 // Success! Return the response. 271 return json_decode( $response_body, true ); 272 } 273 274 // If we get here, all retries failed. 274 $final_response = json_decode( $response_body, true ); 275 break; 276 } 277 278 remove_action( 'http_api_curl', $curl_override, 10 ); 279 280 if ( null !== $final_response ) { 281 return $final_response; 282 } 283 284 if ( $final_error ) { 285 return $final_error; 286 } 287 275 288 if ( $last_error ) { 276 return $last_error;289 return new WP_Error( 'openai_error', 'OpenAI API request failed: ' . $last_error->get_error_message() ); 277 290 } 278 291 … … 363 376 self::DEFAULT_MODEL => 'GPT Image 1.5 (latest model)', 364 377 ]; 378 } 379 380 /** 381 * Gets the estimated image generation time in seconds. 382 * 383 * @param string $quality_setting Optional quality setting. 384 * @param array $additional_params Optional additional parameters for estimation. 385 * @return int Estimated time in seconds. 386 */ 387 public function get_estimated_generation_time( $quality_setting = '', $additional_params = [] ) { 388 $quality = $quality_setting ? $quality_setting : self::get_quality_setting(); 389 $has_source_images = ! empty( $additional_params['source_image_urls'] ) || 390 ! empty( $additional_params['source_image_url'] ) || 391 ! empty( $additional_params['additional_image_urls'] ); 392 393 switch ( $quality ) { 394 case 'low': 395 $base_time = 15; 396 break; 397 case 'high': 398 $base_time = 60; 399 break; 400 case 'medium': 401 default: 402 $base_time = 30; 403 break; 404 } 405 406 if ( $has_source_images ) { 407 return (int) ceil( $base_time * 1.25 ); 408 } 409 410 return $base_time; 411 } 412 413 /** 414 * Resolves the model to use for a request. 415 * 416 * @param string $quality_setting Optional quality setting. 417 * @param array $additional_params Optional additional parameters for the request. 418 * @return string The resolved model identifier. 419 */ 420 public function get_model_for_request( $quality_setting = '', $additional_params = [] ) { 421 return self::DEFAULT_MODEL; 365 422 } 366 423 -
kaigen/trunk/inc/providers/class-image-provider-replicate.php
r3424397 r3428748 9 9 10 10 use KaiGen\Image_Provider; 11 use WP_Error; 11 12 12 13 /** … … 78 79 79 80 $input_data = [ 'prompt' => $prompt ]; 80 81 // Determine which model to use.82 $model_to_use = $this->model;83 81 84 82 // Handle source image URLs (can be single string or array). … … 101 99 102 100 if ( ! empty( $image_inputs ) ) { 103 $model_to_use = $this->get_image_to_image_model();104 101 $input_data['image_input'] = $image_inputs; 105 102 106 103 // Set size to 2K for low quality image edits (seedream-4.5 only supports "2K", "4K", or "custom"). 107 $quality = self::get_quality_setting();104 $quality = $additional_params['quality'] ?? self::get_quality_setting(); 108 105 109 106 if ( 'low' === $quality ) { … … 153 150 ]; 154 151 155 $api_url = self::API_BASE_URL . "{$ model_to_use}/predictions";152 $api_url = self::API_BASE_URL . "{$this->model}/predictions"; 156 153 157 154 // Make initial request with shorter timeout since we're just waiting for the URL. … … 176 173 if ( null === $body && json_last_error() !== JSON_ERROR_NONE ) { 177 174 $raw_body = wp_remote_retrieve_body( $response ); 178 return new \WP_Error(175 return new WP_Error( 179 176 'replicate_api_error', 180 177 'Invalid JSON response from Replicate API. Response code: ' . $response_code . '. Body: ' . substr( $raw_body, 0, 200 ) … … 185 182 if ( ! is_array( $body ) ) { 186 183 $raw_body = wp_remote_retrieve_body( $response ); 187 return new \WP_Error(184 return new WP_Error( 188 185 'replicate_api_error', 189 186 'Unexpected response format from Replicate API. Response code: ' . $response_code . '. Body: ' . substr( $raw_body, 0, 200 ) … … 197 194 $error_message = 'Validation error: ' . ( is_array( $body['detail'] ) ? wp_json_encode( $body['detail'] ) : $body['detail'] ); 198 195 } 199 return new \WP_Error( 'replicate_validation_error', $error_message );196 return new WP_Error( 'replicate_validation_error', $error_message ); 200 197 } 201 198 … … 212 209 strpos( $error_message, 'E005' ) !== false 213 210 ) { 214 return new \WP_Error( 'content_moderation', 'Your prompt contains content that violates AI safety guidelines. Please try rephrasing it.' );211 return new WP_Error( 'content_moderation', 'Your prompt contains content that violates AI safety guidelines. Please try rephrasing it.' ); 215 212 } 216 213 217 214 // Return API errors immediately without retry. 218 return new \WP_Error( 'replicate_api_error', $error_message );215 return new WP_Error( 'replicate_api_error', $error_message ); 219 216 } 220 217 … … 261 258 $raw_body = wp_remote_retrieve_body( $response ); 262 259 $response_code = wp_remote_retrieve_response_code( $response ); 263 return new \WP_Error(260 return new WP_Error( 264 261 'replicate_api_error', 265 262 'Invalid JSON response from Replicate API when checking prediction status. Response code: ' . $response_code . '. Body: ' . substr( $raw_body, 0, 200 ) … … 271 268 $raw_body = wp_remote_retrieve_body( $response ); 272 269 $response_code = wp_remote_retrieve_response_code( $response ); 273 return new \WP_Error(270 return new WP_Error( 274 271 'replicate_api_error', 275 272 'Unexpected response format from Replicate API when checking prediction status. Response code: ' . $response_code . '. Body: ' . substr( $raw_body, 0, 200 ) … … 290 287 291 288 if ( ! is_array( $response ) ) { 292 return new \WP_Error( 'replicate_error', 'Invalid response format from Replicate' );289 return new WP_Error( 'replicate_error', 'Invalid response format from Replicate' ); 293 290 } 294 291 … … 306 303 strpos( $error_message, 'content moderation' ) !== false 307 304 ) { 308 return new \WP_Error(305 return new WP_Error( 309 306 'content_moderation', 310 307 'Your prompt contains content that violates AI safety guidelines. Please try rephrasing it.' … … 314 311 // Check for image-to-image specific errors. 315 312 if ( strpos( $error_message, 'image' ) !== false && strpos( $error_message, 'parameter' ) !== false ) { 316 return new \WP_Error(313 return new WP_Error( 317 314 'image_to_image_error', 318 315 'Image-to-image generation failed: ' . $error_message . '. Please check that your source image is valid and accessible.' … … 322 319 // Check for model-specific errors. 323 320 if ( strpos( $error_message, 'flux-kontext-pro' ) !== false || strpos( $error_message, 'model' ) !== false ) { 324 return new \WP_Error(321 return new WP_Error( 325 322 'model_error', 326 323 'Model error: ' . $error_message . '. The image-to-image model may be temporarily unavailable.' … … 329 326 330 327 // Return the raw error for other cases. 331 return new \WP_Error( 'replicate_error', $error_message );328 return new WP_Error( 'replicate_error', $error_message ); 332 329 } 333 330 … … 349 346 ) { 350 347 $error_message = 'Image-to-image generation failed. Please check that your source image is valid and accessible.'; 351 return new \WP_Error( 'image_to_image_failed', $error_message );348 return new WP_Error( 'image_to_image_failed', $error_message ); 352 349 } 353 350 … … 361 358 ) { 362 359 $error_message = 'Your prompt contains content that violates AI safety guidelines. Please try rephrasing it.'; 363 return new \WP_Error( 'content_moderation', $error_message );360 return new WP_Error( 'content_moderation', $error_message ); 364 361 } 365 362 … … 369 366 } 370 367 371 return new \WP_Error( 'generation_failed', $error_message );368 return new WP_Error( 'generation_failed', $error_message ); 372 369 } 373 370 … … 380 377 // Return pending error with prediction ID for polling. 381 378 if ( isset( $response['id'] ) ) { 382 return new \WP_Error(379 return new WP_Error( 383 380 'replicate_pending', 384 381 'Image generation is still processing', … … 387 384 } 388 385 389 return new \WP_Error( 'replicate_error', 'No image data in response' );386 return new WP_Error( 'replicate_error', 'No image data in response' ); 390 387 } 391 388 … … 408 405 return [ 409 406 'prunaai/hidream-l1-fast' => 'HiDream-I1 Fast by PrunaAI (low quality)', 410 'bytedance/seedream-4.5' => 'Seedream 4.5 by Bytedance ( highquality)',411 'google/nano-banana-pro' => 'Nano Banana Pro by Google (high estquality)',407 'bytedance/seedream-4.5' => 'Seedream 4.5 by Bytedance (medium quality)', 408 'google/nano-banana-pro' => 'Nano Banana Pro by Google (high quality)', 412 409 ]; 413 410 } 414 411 415 412 /** 413 * Gets the estimated image generation time in seconds. 414 * 415 * @param string $quality_setting Optional quality setting. 416 * @param array $additional_params Optional additional parameters for estimation. 417 * @return int Estimated time in seconds. 418 */ 419 public function get_estimated_generation_time( $quality_setting = '', $additional_params = [] ) { 420 $quality = $quality_setting ? $quality_setting : self::get_quality_setting(); 421 $model = $this->model ? $this->model : $this->get_model_from_quality_setting( $quality, $additional_params ); 422 $has_source_images = ! empty( $additional_params['source_image_urls'] ) || 423 ! empty( $additional_params['source_image_url'] ); 424 425 switch ( $model ) { 426 case 'prunaai/hidream-l1-fast': 427 $base_time = 3; 428 break; 429 case 'bytedance/seedream-4.5': 430 $base_time = 20; 431 break; 432 case 'google/nano-banana-pro': 433 $base_time = 40; 434 break; 435 default: 436 $base_time = 30; 437 break; 438 } 439 440 if ( $has_source_images ) { 441 return (int) ceil( $base_time * 1.25 ); 442 } 443 444 return $base_time; 445 } 446 447 /** 448 * Resolves the model to use for a request. 449 * 450 * @param string $quality_setting Optional quality setting. 451 * @param array $additional_params Optional additional parameters for the request. 452 * @return string The resolved model identifier. 453 */ 454 public function get_model_for_request( $quality_setting = '', $additional_params = [] ) { 455 $quality = $quality_setting ? $quality_setting : self::get_quality_setting(); 456 return $this->model ? $this->model : $this->get_model_from_quality_setting( $quality, $additional_params ); 457 } 458 459 /** 416 460 * Gets the image-to-image model for Replicate based on quality setting. 417 461 * 462 * @param string $quality_setting The quality setting. 418 463 * @return string The image-to-image model. 419 464 */ 420 private function get_image_to_image_model( ) {465 private function get_image_to_image_model( $quality_setting ) { 421 466 $model = 'bytedance/seedream-4.5'; 422 $quality = self::get_quality_setting();467 $quality = $quality_setting ? $quality_setting : self::get_quality_setting(); 423 468 424 469 if ( 'high' === $quality ) { … … 433 478 * 434 479 * @param string $quality_setting The quality setting. 480 * @param array $additional_params Optional additional parameters for the request. 435 481 * @return string The model. 436 482 */ 437 public function get_model_from_quality_setting( $quality_setting ) { 483 public function get_model_from_quality_setting( $quality_setting, $additional_params = [] ) { 484 $has_source_images = ! empty( $additional_params['source_image_urls'] ) || 485 ! empty( $additional_params['source_image_url'] ); 486 487 if ( $has_source_images ) { 488 return $this->get_image_to_image_model( $quality_setting ); 489 } 490 438 491 switch ( $quality_setting ) { 439 492 case 'low': … … 465 518 if ( is_wp_error( $head_response ) ) { 466 519 $error_message = 'Image URL not accessible: ' . $head_response->get_error_message(); 467 return new \WP_Error( 'image_not_accessible', $error_message );520 return new WP_Error( 'image_not_accessible', $error_message ); 468 521 } 469 522 … … 493 546 if ( is_wp_error( $response ) ) { 494 547 $error_message = 'Failed to download image: ' . $response->get_error_message(); 495 return new \WP_Error( 'image_download_failed', $error_message );548 return new WP_Error( 'image_download_failed', $error_message ); 496 549 } 497 550 … … 499 552 if ( 200 !== $response_code ) { 500 553 $error_message = "Failed to download image: HTTP {$response_code}"; 501 return new \WP_Error( 'image_download_failed', $error_message );554 return new WP_Error( 'image_download_failed', $error_message ); 502 555 } 503 556 504 557 $image_data = wp_remote_retrieve_body( $response ); 505 558 if ( empty( $image_data ) ) { 506 return new \WP_Error( 'empty_image_data', 'Downloaded image data is empty' );559 return new WP_Error( 'empty_image_data', 'Downloaded image data is empty' ); 507 560 } 508 561 -
kaigen/trunk/kaigen.php
r3424397 r3428748 5 5 * Requires at least: 6.1 6 6 * Requires PHP: 7.0 7 * Version: 0.2. 67 * Version: 0.2.7 8 8 * Author: Jacob Schweitzer 9 9 * License: GPL-2.0-or-later
Note: See TracChangeset
for help on using the changeset viewer.