Plugin Directory

Changeset 3428748


Ignore:
Timestamp:
12/28/2025 10:27:38 PM (3 months ago)
Author:
primetimejas
Message:

Update to version v0.2.7 from GitHub

Location:
kaigen
Files:
22 edited
1 copied

Legend:

Unmodified
Added
Removed
  • kaigen/tags/v0.2.7/.distignore

    r3315977 r3428748  
    1717.gitignore
    1818.gitattributes
    19 CLAUDE.md
    2019README.md
    2120
  • kaigen/tags/v0.2.7/assets/kaigen-admin.css

    r3424397 r3428748  
    8787}
    8888
     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
    89181/* ===== RESPONSIVE ADJUSTMENTS ===== */
    90182
     
    124216
    125217.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 {
    126232    border: 2px solid #007cba;
    127233    background: #f0f8ff;
     
    250356}
    251357
     358.kaigen-modal-quality-container {
     359    display: flex;
     360    gap: 8px;
     361    flex-wrap: wrap;
     362    margin-top: 8px;
     363}
     364
    252365.kaigen-modal-aspect-ratio-icon-container {
    253366    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  
    9494                }
    9595
     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
    96109                // 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;
    98114
    99115                if ( ! isset( $settings['kaigen'] ) ) {
    100116                    $settings['kaigen'] = [];
    101117                }
    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;
    103122
    104123                // Add to editor settings directly.
     
    106125                    $settings['kaigen_settings'] = [];
    107126                }
    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 ] );
    110132
    111133                return $settings;
  • kaigen/tags/v0.2.7/inc/class-image-provider.php

    r3424397 r3428748  
    6060        }
    6161        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;
    6284    }
    6385
  • kaigen/tags/v0.2.7/inc/class-rest-api.php

    r3424397 r3428748  
    77
    88namespace KaiGen;
     9
     10use WP_Error;
    911
    1012/**
     
    99101            ]
    100102        );
     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        );
    101125    }
    102126
     
    121145        $provider_id = $request->get_param( 'provider' );
    122146
     147        // Get additional parameters with defaults.
     148        $additional_params = $this->get_additional_params( $request );
     149
    123150        // Get provider model.
    124         $model = $this->get_provider_model( $provider_id );
     151        $model = $this->get_provider_model( $provider_id, $additional_params );
    125152        if ( is_wp_error( $model ) ) {
    126153            return $model;
    127154        }
    128155
    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
    130186        $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 );
    134276    }
    135277
     
    138280     *
    139281     * @param string $provider_id The provider ID.
     282     * @param array  $additional_params Additional parameters.
    140283     * @return string|WP_Error The model or error.
    141284     */
    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
    155291        $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;
    172307    }
    173308
     
    184319        $quality_settings = get_option( 'kaigen_quality_settings', [] );
    185320        $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        }
    186325
    187326        $defaults = [
     
    201340            $params[ $key ] = $request->get_param( $key ) ?? $default;
    202341        }
     342        $params['quality'] = $quality;
    203343
    204344        // Add source image URL if provided (single or array).
     
    276416                    if ( isset( $result['status'] ) && 'failed' === $result['status'] ) {
    277417                        if ( isset( $result['error'] ) && strpos( $result['error'], 'flagged by safety filters' ) !== false ) {
    278                             return new \WP_Error(
     418                            return new WP_Error(
    279419                                'content_filtered',
    280420                                'The image was flagged by the provider\'s safety filters. Please modify your prompt and try again.',
     
    357497
    358498                if ( $retry_count >= $max_retries ) {
    359                     return new \WP_Error(
     499                    return new WP_Error(
    360500                        'api_error',
    361501                        'Failed after ' . $max_retries . ' attempts: ' . $e->getMessage(),
     
    382522        $provider = kaigen_provider_manager()->get_provider( $provider_id );
    383523        if ( ! $provider ) {
    384             return new \WP_Error( 'invalid_provider', "Invalid provider: {$provider_id}" );
     524            return new WP_Error( 'invalid_provider', "Invalid provider: {$provider_id}" );
    385525        }
    386526
  • kaigen/tags/v0.2.7/inc/interface-image-provider.php

    r3424397 r3428748  
    8585     */
    8686    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 = [] );
    87105}
  • kaigen/tags/v0.2.7/inc/providers/class-image-provider-openai.php

    r3424397 r3428748  
    99
    1010use KaiGen\Image_Provider;
     11use WP_Error;
    1112
    1213/**
     
    7374
    7475        $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.
    7677        $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         );
    10978
    11079        // Default to API_BASE_URL.
     
    11685        }
    11786
    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        }
    12097
    12198        // Map quality settings to supported values.
     
    156133            $body .= $quality . "\r\n";
    157134
     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
    158140            // Add format parameter (jpeg is faster than png).
    159141            $body .= "--{$boundary}\r\n";
     
    184166                'prompt'        => $prompt,
    185167                'quality'       => $quality,
     168                'moderation'    => 'low',
    186169                'output_format' => 'jpeg',
    187170            ];
     
    197180
    198181        // 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 );
    201203
    202204        while ( $attempt < $max_retries ) {
     
    224226                }
    225227
    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;
    228230            }
    229231
     
    232234
    233235            if ( 200 !== $response_code ) {
    234 
    235236                // Parse error response.
    236237                $error_data = json_decode( $response_body, true );
     
    240241                    // Check for specific error about image URL in prompt.
    241242                    if ( strpos( $error_message, 'image URL' ) !== false ) {
    242 
    243243                        // Remove the image URL from the body and retry.
    244244                        $body['prompt'] = $prompt;
     
    257257
    258258                            if ( $retry_code < 400 ) {
    259                                 return json_decode( $retry_body, true );
     259                                $final_response = json_decode( $retry_body, true );
     260                                break;
    260261                            }
    261262                        }
    262263                    }
    263264
    264                     return new WP_Error( 'openai_error', $error_message );
     265                    $final_error = new WP_Error( 'openai_error', $error_message );
     266                    break;
    265267                }
    266268
    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;
    268271            }
    269272
    270273            // 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
    275288        if ( $last_error ) {
    276             return $last_error;
     289            return new WP_Error( 'openai_error', 'OpenAI API request failed: ' . $last_error->get_error_message() );
    277290        }
    278291
     
    363376            self::DEFAULT_MODEL => 'GPT Image 1.5 (latest model)',
    364377        ];
     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;
    365422    }
    366423
  • kaigen/tags/v0.2.7/inc/providers/class-image-provider-replicate.php

    r3424397 r3428748  
    99
    1010use KaiGen\Image_Provider;
     11use WP_Error;
    1112
    1213/**
     
    7879
    7980        $input_data = [ 'prompt' => $prompt ];
    80 
    81         // Determine which model to use.
    82         $model_to_use = $this->model;
    8381
    8482        // Handle source image URLs (can be single string or array).
     
    10199
    102100            if ( ! empty( $image_inputs ) ) {
    103                 $model_to_use              = $this->get_image_to_image_model();
    104101                $input_data['image_input'] = $image_inputs;
    105102
    106103                // 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();
    108105
    109106                if ( 'low' === $quality ) {
     
    153150        ];
    154151
    155         $api_url = self::API_BASE_URL . "{$model_to_use}/predictions";
     152        $api_url = self::API_BASE_URL . "{$this->model}/predictions";
    156153
    157154        // Make initial request with shorter timeout since we're just waiting for the URL.
     
    176173        if ( null === $body && json_last_error() !== JSON_ERROR_NONE ) {
    177174            $raw_body = wp_remote_retrieve_body( $response );
    178             return new \WP_Error(
     175            return new WP_Error(
    179176                'replicate_api_error',
    180177                'Invalid JSON response from Replicate API. Response code: ' . $response_code . '. Body: ' . substr( $raw_body, 0, 200 )
     
    185182        if ( ! is_array( $body ) ) {
    186183            $raw_body = wp_remote_retrieve_body( $response );
    187             return new \WP_Error(
     184            return new WP_Error(
    188185                'replicate_api_error',
    189186                'Unexpected response format from Replicate API. Response code: ' . $response_code . '. Body: ' . substr( $raw_body, 0, 200 )
     
    197194                $error_message = 'Validation error: ' . ( is_array( $body['detail'] ) ? wp_json_encode( $body['detail'] ) : $body['detail'] );
    198195            }
    199             return new \WP_Error( 'replicate_validation_error', $error_message );
     196            return new WP_Error( 'replicate_validation_error', $error_message );
    200197        }
    201198
     
    212209                strpos( $error_message, 'E005' ) !== false
    213210            ) {
    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.' );
    215212            }
    216213
    217214            // 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 );
    219216        }
    220217
     
    261258            $raw_body      = wp_remote_retrieve_body( $response );
    262259            $response_code = wp_remote_retrieve_response_code( $response );
    263             return new \WP_Error(
     260            return new WP_Error(
    264261                'replicate_api_error',
    265262                'Invalid JSON response from Replicate API when checking prediction status. Response code: ' . $response_code . '. Body: ' . substr( $raw_body, 0, 200 )
     
    271268            $raw_body      = wp_remote_retrieve_body( $response );
    272269            $response_code = wp_remote_retrieve_response_code( $response );
    273             return new \WP_Error(
     270            return new WP_Error(
    274271                'replicate_api_error',
    275272                'Unexpected response format from Replicate API when checking prediction status. Response code: ' . $response_code . '. Body: ' . substr( $raw_body, 0, 200 )
     
    290287
    291288        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' );
    293290        }
    294291
     
    306303                strpos( $error_message, 'content moderation' ) !== false
    307304            ) {
    308                 return new \WP_Error(
     305                return new WP_Error(
    309306                    'content_moderation',
    310307                    'Your prompt contains content that violates AI safety guidelines. Please try rephrasing it.'
     
    314311            // Check for image-to-image specific errors.
    315312            if ( strpos( $error_message, 'image' ) !== false && strpos( $error_message, 'parameter' ) !== false ) {
    316                 return new \WP_Error(
     313                return new WP_Error(
    317314                    'image_to_image_error',
    318315                    'Image-to-image generation failed: ' . $error_message . '. Please check that your source image is valid and accessible.'
     
    322319            // Check for model-specific errors.
    323320            if ( strpos( $error_message, 'flux-kontext-pro' ) !== false || strpos( $error_message, 'model' ) !== false ) {
    324                 return new \WP_Error(
     321                return new WP_Error(
    325322                    'model_error',
    326323                    'Model error: ' . $error_message . '. The image-to-image model may be temporarily unavailable.'
     
    329326
    330327            // 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 );
    332329        }
    333330
     
    349346            ) {
    350347                $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 );
    352349            }
    353350
     
    361358            ) {
    362359                $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 );
    364361            }
    365362
     
    369366            }
    370367
    371             return new \WP_Error( 'generation_failed', $error_message );
     368            return new WP_Error( 'generation_failed', $error_message );
    372369        }
    373370
     
    380377        // Return pending error with prediction ID for polling.
    381378        if ( isset( $response['id'] ) ) {
    382             return new \WP_Error(
     379            return new WP_Error(
    383380                'replicate_pending',
    384381                'Image generation is still processing',
     
    387384        }
    388385
    389         return new \WP_Error( 'replicate_error', 'No image data in response' );
     386        return new WP_Error( 'replicate_error', 'No image data in response' );
    390387    }
    391388
     
    408405        return [
    409406            'prunaai/hidream-l1-fast' => 'HiDream-I1 Fast by PrunaAI (low quality)',
    410             'bytedance/seedream-4.5'  => 'Seedream 4.5 by Bytedance (high quality)',
    411             'google/nano-banana-pro'  => 'Nano Banana Pro by Google (highest quality)',
     407            'bytedance/seedream-4.5'  => 'Seedream 4.5 by Bytedance (medium quality)',
     408            'google/nano-banana-pro'  => 'Nano Banana Pro by Google (high quality)',
    412409        ];
    413410    }
    414411
    415412    /**
     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    /**
    416460     * Gets the image-to-image model for Replicate based on quality setting.
    417461     *
     462     * @param string $quality_setting The quality setting.
    418463     * @return string The image-to-image model.
    419464     */
    420     private function get_image_to_image_model() {
     465    private function get_image_to_image_model( $quality_setting ) {
    421466        $model   = 'bytedance/seedream-4.5';
    422         $quality = self::get_quality_setting();
     467        $quality = $quality_setting ? $quality_setting : self::get_quality_setting();
    423468
    424469        if ( 'high' === $quality ) {
     
    433478     *
    434479     * @param string $quality_setting The quality setting.
     480     * @param array  $additional_params Optional additional parameters for the request.
    435481     * @return string The model.
    436482     */
    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
    438491        switch ( $quality_setting ) {
    439492            case 'low':
     
    465518        if ( is_wp_error( $head_response ) ) {
    466519            $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 );
    468521        }
    469522
     
    493546        if ( is_wp_error( $response ) ) {
    494547            $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 );
    496549        }
    497550
     
    499552        if ( 200 !== $response_code ) {
    500553            $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 );
    502555        }
    503556
    504557        $image_data = wp_remote_retrieve_body( $response );
    505558        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' );
    507560        }
    508561
  • kaigen/tags/v0.2.7/kaigen.php

    r3424397 r3428748  
    55 * Requires at least: 6.1
    66 * Requires PHP:      7.0
    7  * Version:           0.2.6
     7 * Version:           0.2.7
    88 * Author:            Jacob Schweitzer
    99 * License:           GPL-2.0-or-later
  • kaigen/trunk/.distignore

    r3315977 r3428748  
    1717.gitignore
    1818.gitattributes
    19 CLAUDE.md
    2019README.md
    2120
  • kaigen/trunk/assets/kaigen-admin.css

    r3424397 r3428748  
    8787}
    8888
     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
    89181/* ===== RESPONSIVE ADJUSTMENTS ===== */
    90182
     
    124216
    125217.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 {
    126232    border: 2px solid #007cba;
    127233    background: #f0f8ff;
     
    250356}
    251357
     358.kaigen-modal-quality-container {
     359    display: flex;
     360    gap: 8px;
     361    flex-wrap: wrap;
     362    margin-top: 8px;
     363}
     364
    252365.kaigen-modal-aspect-ratio-icon-container {
    253366    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  
    9494                }
    9595
     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
    96109                // 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;
    98114
    99115                if ( ! isset( $settings['kaigen'] ) ) {
    100116                    $settings['kaigen'] = [];
    101117                }
    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;
    103122
    104123                // Add to editor settings directly.
     
    106125                    $settings['kaigen_settings'] = [];
    107126                }
    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 ] );
    110132
    111133                return $settings;
  • kaigen/trunk/inc/class-image-provider.php

    r3424397 r3428748  
    6060        }
    6161        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;
    6284    }
    6385
  • kaigen/trunk/inc/class-rest-api.php

    r3424397 r3428748  
    77
    88namespace KaiGen;
     9
     10use WP_Error;
    911
    1012/**
     
    99101            ]
    100102        );
     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        );
    101125    }
    102126
     
    121145        $provider_id = $request->get_param( 'provider' );
    122146
     147        // Get additional parameters with defaults.
     148        $additional_params = $this->get_additional_params( $request );
     149
    123150        // Get provider model.
    124         $model = $this->get_provider_model( $provider_id );
     151        $model = $this->get_provider_model( $provider_id, $additional_params );
    125152        if ( is_wp_error( $model ) ) {
    126153            return $model;
    127154        }
    128155
    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
    130186        $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 );
    134276    }
    135277
     
    138280     *
    139281     * @param string $provider_id The provider ID.
     282     * @param array  $additional_params Additional parameters.
    140283     * @return string|WP_Error The model or error.
    141284     */
    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
    155291        $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;
    172307    }
    173308
     
    184319        $quality_settings = get_option( 'kaigen_quality_settings', [] );
    185320        $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        }
    186325
    187326        $defaults = [
     
    201340            $params[ $key ] = $request->get_param( $key ) ?? $default;
    202341        }
     342        $params['quality'] = $quality;
    203343
    204344        // Add source image URL if provided (single or array).
     
    276416                    if ( isset( $result['status'] ) && 'failed' === $result['status'] ) {
    277417                        if ( isset( $result['error'] ) && strpos( $result['error'], 'flagged by safety filters' ) !== false ) {
    278                             return new \WP_Error(
     418                            return new WP_Error(
    279419                                'content_filtered',
    280420                                'The image was flagged by the provider\'s safety filters. Please modify your prompt and try again.',
     
    357497
    358498                if ( $retry_count >= $max_retries ) {
    359                     return new \WP_Error(
     499                    return new WP_Error(
    360500                        'api_error',
    361501                        'Failed after ' . $max_retries . ' attempts: ' . $e->getMessage(),
     
    382522        $provider = kaigen_provider_manager()->get_provider( $provider_id );
    383523        if ( ! $provider ) {
    384             return new \WP_Error( 'invalid_provider', "Invalid provider: {$provider_id}" );
     524            return new WP_Error( 'invalid_provider', "Invalid provider: {$provider_id}" );
    385525        }
    386526
  • kaigen/trunk/inc/interface-image-provider.php

    r3424397 r3428748  
    8585     */
    8686    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 = [] );
    87105}
  • kaigen/trunk/inc/providers/class-image-provider-openai.php

    r3424397 r3428748  
    99
    1010use KaiGen\Image_Provider;
     11use WP_Error;
    1112
    1213/**
     
    7374
    7475        $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.
    7677        $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         );
    10978
    11079        // Default to API_BASE_URL.
     
    11685        }
    11786
    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        }
    12097
    12198        // Map quality settings to supported values.
     
    156133            $body .= $quality . "\r\n";
    157134
     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
    158140            // Add format parameter (jpeg is faster than png).
    159141            $body .= "--{$boundary}\r\n";
     
    184166                'prompt'        => $prompt,
    185167                'quality'       => $quality,
     168                'moderation'    => 'low',
    186169                'output_format' => 'jpeg',
    187170            ];
     
    197180
    198181        // 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 );
    201203
    202204        while ( $attempt < $max_retries ) {
     
    224226                }
    225227
    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;
    228230            }
    229231
     
    232234
    233235            if ( 200 !== $response_code ) {
    234 
    235236                // Parse error response.
    236237                $error_data = json_decode( $response_body, true );
     
    240241                    // Check for specific error about image URL in prompt.
    241242                    if ( strpos( $error_message, 'image URL' ) !== false ) {
    242 
    243243                        // Remove the image URL from the body and retry.
    244244                        $body['prompt'] = $prompt;
     
    257257
    258258                            if ( $retry_code < 400 ) {
    259                                 return json_decode( $retry_body, true );
     259                                $final_response = json_decode( $retry_body, true );
     260                                break;
    260261                            }
    261262                        }
    262263                    }
    263264
    264                     return new WP_Error( 'openai_error', $error_message );
     265                    $final_error = new WP_Error( 'openai_error', $error_message );
     266                    break;
    265267                }
    266268
    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;
    268271            }
    269272
    270273            // 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
    275288        if ( $last_error ) {
    276             return $last_error;
     289            return new WP_Error( 'openai_error', 'OpenAI API request failed: ' . $last_error->get_error_message() );
    277290        }
    278291
     
    363376            self::DEFAULT_MODEL => 'GPT Image 1.5 (latest model)',
    364377        ];
     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;
    365422    }
    366423
  • kaigen/trunk/inc/providers/class-image-provider-replicate.php

    r3424397 r3428748  
    99
    1010use KaiGen\Image_Provider;
     11use WP_Error;
    1112
    1213/**
     
    7879
    7980        $input_data = [ 'prompt' => $prompt ];
    80 
    81         // Determine which model to use.
    82         $model_to_use = $this->model;
    8381
    8482        // Handle source image URLs (can be single string or array).
     
    10199
    102100            if ( ! empty( $image_inputs ) ) {
    103                 $model_to_use              = $this->get_image_to_image_model();
    104101                $input_data['image_input'] = $image_inputs;
    105102
    106103                // 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();
    108105
    109106                if ( 'low' === $quality ) {
     
    153150        ];
    154151
    155         $api_url = self::API_BASE_URL . "{$model_to_use}/predictions";
     152        $api_url = self::API_BASE_URL . "{$this->model}/predictions";
    156153
    157154        // Make initial request with shorter timeout since we're just waiting for the URL.
     
    176173        if ( null === $body && json_last_error() !== JSON_ERROR_NONE ) {
    177174            $raw_body = wp_remote_retrieve_body( $response );
    178             return new \WP_Error(
     175            return new WP_Error(
    179176                'replicate_api_error',
    180177                'Invalid JSON response from Replicate API. Response code: ' . $response_code . '. Body: ' . substr( $raw_body, 0, 200 )
     
    185182        if ( ! is_array( $body ) ) {
    186183            $raw_body = wp_remote_retrieve_body( $response );
    187             return new \WP_Error(
     184            return new WP_Error(
    188185                'replicate_api_error',
    189186                'Unexpected response format from Replicate API. Response code: ' . $response_code . '. Body: ' . substr( $raw_body, 0, 200 )
     
    197194                $error_message = 'Validation error: ' . ( is_array( $body['detail'] ) ? wp_json_encode( $body['detail'] ) : $body['detail'] );
    198195            }
    199             return new \WP_Error( 'replicate_validation_error', $error_message );
     196            return new WP_Error( 'replicate_validation_error', $error_message );
    200197        }
    201198
     
    212209                strpos( $error_message, 'E005' ) !== false
    213210            ) {
    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.' );
    215212            }
    216213
    217214            // 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 );
    219216        }
    220217
     
    261258            $raw_body      = wp_remote_retrieve_body( $response );
    262259            $response_code = wp_remote_retrieve_response_code( $response );
    263             return new \WP_Error(
     260            return new WP_Error(
    264261                'replicate_api_error',
    265262                'Invalid JSON response from Replicate API when checking prediction status. Response code: ' . $response_code . '. Body: ' . substr( $raw_body, 0, 200 )
     
    271268            $raw_body      = wp_remote_retrieve_body( $response );
    272269            $response_code = wp_remote_retrieve_response_code( $response );
    273             return new \WP_Error(
     270            return new WP_Error(
    274271                'replicate_api_error',
    275272                'Unexpected response format from Replicate API when checking prediction status. Response code: ' . $response_code . '. Body: ' . substr( $raw_body, 0, 200 )
     
    290287
    291288        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' );
    293290        }
    294291
     
    306303                strpos( $error_message, 'content moderation' ) !== false
    307304            ) {
    308                 return new \WP_Error(
     305                return new WP_Error(
    309306                    'content_moderation',
    310307                    'Your prompt contains content that violates AI safety guidelines. Please try rephrasing it.'
     
    314311            // Check for image-to-image specific errors.
    315312            if ( strpos( $error_message, 'image' ) !== false && strpos( $error_message, 'parameter' ) !== false ) {
    316                 return new \WP_Error(
     313                return new WP_Error(
    317314                    'image_to_image_error',
    318315                    'Image-to-image generation failed: ' . $error_message . '. Please check that your source image is valid and accessible.'
     
    322319            // Check for model-specific errors.
    323320            if ( strpos( $error_message, 'flux-kontext-pro' ) !== false || strpos( $error_message, 'model' ) !== false ) {
    324                 return new \WP_Error(
     321                return new WP_Error(
    325322                    'model_error',
    326323                    'Model error: ' . $error_message . '. The image-to-image model may be temporarily unavailable.'
     
    329326
    330327            // 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 );
    332329        }
    333330
     
    349346            ) {
    350347                $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 );
    352349            }
    353350
     
    361358            ) {
    362359                $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 );
    364361            }
    365362
     
    369366            }
    370367
    371             return new \WP_Error( 'generation_failed', $error_message );
     368            return new WP_Error( 'generation_failed', $error_message );
    372369        }
    373370
     
    380377        // Return pending error with prediction ID for polling.
    381378        if ( isset( $response['id'] ) ) {
    382             return new \WP_Error(
     379            return new WP_Error(
    383380                'replicate_pending',
    384381                'Image generation is still processing',
     
    387384        }
    388385
    389         return new \WP_Error( 'replicate_error', 'No image data in response' );
     386        return new WP_Error( 'replicate_error', 'No image data in response' );
    390387    }
    391388
     
    408405        return [
    409406            'prunaai/hidream-l1-fast' => 'HiDream-I1 Fast by PrunaAI (low quality)',
    410             'bytedance/seedream-4.5'  => 'Seedream 4.5 by Bytedance (high quality)',
    411             'google/nano-banana-pro'  => 'Nano Banana Pro by Google (highest quality)',
     407            'bytedance/seedream-4.5'  => 'Seedream 4.5 by Bytedance (medium quality)',
     408            'google/nano-banana-pro'  => 'Nano Banana Pro by Google (high quality)',
    412409        ];
    413410    }
    414411
    415412    /**
     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    /**
    416460     * Gets the image-to-image model for Replicate based on quality setting.
    417461     *
     462     * @param string $quality_setting The quality setting.
    418463     * @return string The image-to-image model.
    419464     */
    420     private function get_image_to_image_model() {
     465    private function get_image_to_image_model( $quality_setting ) {
    421466        $model   = 'bytedance/seedream-4.5';
    422         $quality = self::get_quality_setting();
     467        $quality = $quality_setting ? $quality_setting : self::get_quality_setting();
    423468
    424469        if ( 'high' === $quality ) {
     
    433478     *
    434479     * @param string $quality_setting The quality setting.
     480     * @param array  $additional_params Optional additional parameters for the request.
    435481     * @return string The model.
    436482     */
    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
    438491        switch ( $quality_setting ) {
    439492            case 'low':
     
    465518        if ( is_wp_error( $head_response ) ) {
    466519            $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 );
    468521        }
    469522
     
    493546        if ( is_wp_error( $response ) ) {
    494547            $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 );
    496549        }
    497550
     
    499552        if ( 200 !== $response_code ) {
    500553            $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 );
    502555        }
    503556
    504557        $image_data = wp_remote_retrieve_body( $response );
    505558        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' );
    507560        }
    508561
  • kaigen/trunk/kaigen.php

    r3424397 r3428748  
    55 * Requires at least: 6.1
    66 * Requires PHP:      7.0
    7  * Version:           0.2.6
     7 * Version:           0.2.7
    88 * Author:            Jacob Schweitzer
    99 * License:           GPL-2.0-or-later
Note: See TracChangeset for help on using the changeset viewer.