diff --git a/core-web/libs/data-access/src/lib/dot-ai/dot-ai.service.ts b/core-web/libs/data-access/src/lib/dot-ai/dot-ai.service.ts index 1e712b3d6698..79c4f0e185d3 100644 --- a/core-web/libs/data-access/src/lib/dot-ai/dot-ai.service.ts +++ b/core-web/libs/data-access/src/lib/dot-ai/dot-ai.service.ts @@ -9,7 +9,6 @@ import { DotCMSContentlet, AiPluginResponse, DotAIImageContent, - DotAIImageOrientation, DotAIImageResponse, DotAiProviderConfig } from '@dotcms/dotcms-models'; @@ -80,7 +79,7 @@ export class DotAiService { */ public generateAndPublishImage( prompt: string, - size: string = DotAIImageOrientation.HORIZONTAL + size = '1024x1024' ): Observable { return this.#http .post( diff --git a/core-web/libs/dotcms-models/src/lib/dot-ai.model.ts b/core-web/libs/dotcms-models/src/lib/dot-ai.model.ts index 9bbb46e12626..ea52a95e0663 100644 --- a/core-web/libs/dotcms-models/src/lib/dot-ai.model.ts +++ b/core-web/libs/dotcms-models/src/lib/dot-ai.model.ts @@ -49,7 +49,7 @@ export interface DotAIImageResponse { export interface AIImagePrompt { text: string; type: PromptType; - size: DotAIImageOrientation; + size: string; } /** diff --git a/core-web/libs/ui/src/lib/components/dot-ai-image-prompt/ai-image-prompt.component.html b/core-web/libs/ui/src/lib/components/dot-ai-image-prompt/ai-image-prompt.component.html index 6b6ebbde2019..bd6bf6aa2cd4 100644 --- a/core-web/libs/ui/src/lib/components/dot-ai-image-prompt/ai-image-prompt.component.html +++ b/core-web/libs/ui/src/lib/components/dot-ai-image-prompt/ai-image-prompt.component.html @@ -15,7 +15,7 @@ [isLoading]="store.isLoading()" [images]="store.images()" [activeImageIndex]="store.galleryActiveIndex()" - [orientation]="store.formValue()?.size" /> + [size]="store.formValue()?.size" /> @if (store.isLoading() || store.hasImages()) {
-
diff --git a/core-web/libs/ui/src/lib/components/dot-ai-image-prompt/components/ai-image-prompt-form/ai-image-prompt-form.component.spec.ts b/core-web/libs/ui/src/lib/components/dot-ai-image-prompt/components/ai-image-prompt-form/ai-image-prompt-form.component.spec.ts index 266cead7d4bd..8db73557ec76 100644 --- a/core-web/libs/ui/src/lib/components/dot-ai-image-prompt/components/ai-image-prompt-form/ai-image-prompt-form.component.spec.ts +++ b/core-web/libs/ui/src/lib/components/dot-ai-image-prompt/components/ai-image-prompt-form/ai-image-prompt-form.component.spec.ts @@ -6,7 +6,7 @@ import { ReactiveFormsModule } from '@angular/forms'; import { ButtonModule } from 'primeng/button'; import { DotMessageService } from '@dotcms/data-access'; -import { DotAIImageOrientation, DotGeneratedAIImage, PromptType } from '@dotcms/dotcms-models'; +import { DotGeneratedAIImage, PromptType } from '@dotcms/dotcms-models'; import { AiImagePromptFormComponent } from './ai-image-prompt-form.component'; @@ -17,7 +17,7 @@ import { DotCopyButtonComponent } from '../../../dot-copy-button/dot-copy-button const MOCK_FORM_VALUE = { text: 'Test', type: PromptType.INPUT, - size: DotAIImageOrientation.HORIZONTAL + size: '1024x1024' }; const MOCK_AI_VALUE = { @@ -61,9 +61,7 @@ describe('DotAiImagePromptFormComponent', () => { spectator.detectChanges(); expect(spectator.component.form.get('text').value).toEqual(''); expect(spectator.component.form.get('type').value).toEqual(PromptType.INPUT); - expect(spectator.component.form.get('size').value).toEqual( - DotAIImageOrientation.HORIZONTAL - ); + expect(spectator.component.form.get('size').value).toEqual('1024x1024'); }); it('should emit value when form value change', () => { diff --git a/core-web/libs/ui/src/lib/components/dot-ai-image-prompt/components/ai-image-prompt-form/ai-image-prompt-form.component.ts b/core-web/libs/ui/src/lib/components/dot-ai-image-prompt/components/ai-image-prompt-form/ai-image-prompt-form.component.ts index 7db1696f316e..a2ebc64992ab 100644 --- a/core-web/libs/ui/src/lib/components/dot-ai-image-prompt/components/ai-image-prompt-form/ai-image-prompt-form.component.ts +++ b/core-web/libs/ui/src/lib/components/dot-ai-image-prompt/components/ai-image-prompt-form/ai-image-prompt-form.component.ts @@ -20,21 +20,15 @@ import { } from '@angular/forms'; import { Accordion, AccordionPanel, AccordionHeader, AccordionContent } from 'primeng/accordion'; -import { SelectItem } from 'primeng/api'; import { Button } from 'primeng/button'; +import { InputTextModule } from 'primeng/inputtext'; import { RadioButton } from 'primeng/radiobutton'; -import { Select } from 'primeng/select'; import { Textarea } from 'primeng/textarea'; import { filter } from 'rxjs/operators'; import { DotMessageService } from '@dotcms/data-access'; -import { - AIImagePrompt, - DotAIImageOrientation, - DotGeneratedAIImage, - PromptType -} from '@dotcms/dotcms-models'; +import { AIImagePrompt, DotGeneratedAIImage, PromptType } from '@dotcms/dotcms-models'; import { DotCopyButtonComponent } from './../../../../components/dot-copy-button/dot-copy-button.component'; import { DotFieldRequiredDirective } from './../../../../dot-field-required/dot-field-required.directive'; @@ -53,7 +47,7 @@ import { DotValidators } from './../../../../validators/dotValidators'; RadioButton, ReactiveFormsModule, FormsModule, - Select, + InputTextModule, Textarea, DotFieldRequiredDirective, DotMessagePipe, @@ -95,24 +89,6 @@ export class AiImagePromptFormComponent implements OnChanges { submitButtonLabel = 'block-editor.extension.ai-image.generate'; requiredPrompt = true; tooltipText: string = null; - orientationOptions: SelectItem[] = [ - { - value: DotAIImageOrientation.HORIZONTAL, - label: this.dotMessageService.get( - 'block-editor.extension.ai-image.orientation.horizontal' - ) - }, - { - value: DotAIImageOrientation.SQUARE, - label: this.dotMessageService.get('block-editor.extension.ai-image.orientation.square') - }, - { - value: DotAIImageOrientation.VERTICAL, - label: this.dotMessageService.get( - 'block-editor.extension.ai-image.orientation.vertical' - ) - } - ]; private isUpdatingValidators = false; private destroyRef = inject(DestroyRef); @@ -134,7 +110,10 @@ export class AiImagePromptFormComponent implements OnChanges { this.form = new FormGroup({ text: new FormControl('', [Validators.required, DotValidators.noWhitespace]), type: new FormControl(PromptType.INPUT, Validators.required), - size: new FormControl(DotAIImageOrientation.HORIZONTAL, Validators.required) + size: new FormControl('1024x1024', [ + Validators.required, + Validators.pattern(/^[1-9]\d{1,3}x[1-9]\d{1,3}$/) + ]) }); const typeControl = this.form.get('type'); diff --git a/core-web/libs/ui/src/lib/components/dot-ai-image-prompt/components/ai-image-prompt-gallery/ai-image-prompt-gallery.component.html b/core-web/libs/ui/src/lib/components/dot-ai-image-prompt/components/ai-image-prompt-gallery/ai-image-prompt-gallery.component.html index 760a09021838..0c02aa5b0f1a 100644 --- a/core-web/libs/ui/src/lib/components/dot-ai-image-prompt/components/ai-image-prompt-gallery/ai-image-prompt-gallery.component.html +++ b/core-web/libs/ui/src/lib/components/dot-ai-image-prompt/components/ai-image-prompt-gallery/ai-image-prompt-gallery.component.html @@ -1,6 +1,6 @@ @if (!isLoading && !images?.length) {
{ expect(store.formValue()).toEqual({ text: '', type: PromptType.INPUT, - size: DotAIImageOrientation.HORIZONTAL + size: '1024x1024' }); }); }); @@ -80,7 +80,7 @@ describe('DotAiImagePromptStore', () => { const formValue = { text: 'new text', type: PromptType.INPUT, - size: DotAIImageOrientation.VERTICAL + size: '1024x1792' }; store.setFormValue(formValue); expect(store.formValue()).toEqual(formValue); @@ -92,13 +92,13 @@ describe('DotAiImagePromptStore', () => { store.setFormValue({ text: 'prompt', type: PromptType.INPUT, - size: DotAIImageOrientation.HORIZONTAL + size: '1792x1024' }); store.generateImage(); expect(dotAiService.generateAndPublishImage).toHaveBeenCalledWith( 'prompt', - DotAIImageOrientation.HORIZONTAL + '1792x1024' ); expect(store.status()).toBe(ComponentStatus.IDLE); expect(store.images().length).toBe(1); @@ -110,13 +110,13 @@ describe('DotAiImagePromptStore', () => { store.setFormValue({ text: 'prompt', type: PromptType.INPUT, - size: DotAIImageOrientation.HORIZONTAL + size: '1792x1024' }); store.generateImage(); expect(dotAiService.generateAndPublishImage).toHaveBeenCalledWith( 'prompt', - DotAIImageOrientation.HORIZONTAL + '1792x1024' ); expect(store.status()).toBe(ComponentStatus.ERROR); expect(store.error()).toBe('error'); diff --git a/core-web/libs/ui/src/lib/components/dot-ai-image-prompt/store/ai-image-prompt.store.ts b/core-web/libs/ui/src/lib/components/dot-ai-image-prompt/store/ai-image-prompt.store.ts index 788f42f8cc99..dc5f75a1c4a9 100644 --- a/core-web/libs/ui/src/lib/components/dot-ai-image-prompt/store/ai-image-prompt.store.ts +++ b/core-web/libs/ui/src/lib/components/dot-ai-image-prompt/store/ai-image-prompt.store.ts @@ -11,7 +11,6 @@ import { DotAiService } from '@dotcms/data-access'; import { AIImagePrompt, ComponentStatus, - DotAIImageOrientation, DotGeneratedAIImage, PromptType } from '@dotcms/dotcms-models'; @@ -34,7 +33,7 @@ const initialState: AiImagePromptdState = { formValue: { text: '', type: PromptType.INPUT, - size: DotAIImageOrientation.HORIZONTAL + size: '1024x1024' } }; diff --git a/dotCMS/src/main/java/com/dotcms/ai/client/langchain4j/LangChain4jAIClient.java b/dotCMS/src/main/java/com/dotcms/ai/client/langchain4j/LangChain4jAIClient.java index 06fd2b541e50..a475db263b62 100644 --- a/dotCMS/src/main/java/com/dotcms/ai/client/langchain4j/LangChain4jAIClient.java +++ b/dotCMS/src/main/java/com/dotcms/ai/client/langchain4j/LangChain4jAIClient.java @@ -16,8 +16,8 @@ import com.dotmarketing.util.json.JSONObject; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; -import com.google.common.cache.Cache; -import com.google.common.cache.CacheBuilder; +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.Caffeine; import dev.langchain4j.data.embedding.Embedding; import dev.langchain4j.data.image.Image; import dev.langchain4j.data.message.AiMessage; @@ -47,12 +47,9 @@ import java.util.List; import java.util.function.Function; import java.util.concurrent.CountDownLatch; -import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; -import com.google.common.util.concurrent.UncheckedExecutionException; - /** * {@link AIClient} implementation backed by LangChain4J. * @@ -76,16 +73,16 @@ public class LangChain4jAIClient implements AIClient { private static final long MODEL_CACHE_TTL_HOURS = 1; private static final long STREAMING_TIMEOUT_SECONDS = 300; - private final Cache chatModelCache = CacheBuilder.newBuilder() + private final Cache chatModelCache = Caffeine.newBuilder() .expireAfterWrite(MODEL_CACHE_TTL_HOURS, TimeUnit.HOURS) .build(); - private final Cache streamingChatModelCache = CacheBuilder.newBuilder() + private final Cache streamingChatModelCache = Caffeine.newBuilder() .expireAfterWrite(MODEL_CACHE_TTL_HOURS, TimeUnit.HOURS) .build(); - private final Cache embeddingModelCache = CacheBuilder.newBuilder() + private final Cache embeddingModelCache = Caffeine.newBuilder() .expireAfterWrite(MODEL_CACHE_TTL_HOURS, TimeUnit.HOURS) .build(); - private final Cache imageModelCache = CacheBuilder.newBuilder() + private final Cache imageModelCache = Caffeine.newBuilder() .expireAfterWrite(MODEL_CACHE_TTL_HOURS, TimeUnit.HOURS) .build(); @@ -199,13 +196,12 @@ private StreamingChatModel initStreamingModel( final ProviderConfig modelConfig = ImmutableProviderConfig.copyOf(baseConfig).withModel(modelName); return streamingChatModelCache.get( cacheKeyPrefix + ":chat:streaming:" + modelName, - () -> LangChain4jModelFactory.buildStreamingChatModel(modelConfig)); - } catch (ExecutionException | UncheckedExecutionException e) { - final Throwable cause = e.getCause() != null ? e.getCause() : e; + k -> LangChain4jModelFactory.buildStreamingChatModel(modelConfig)); + } catch (RuntimeException e) { lastException = new IllegalArgumentException( - "Failed to initialize streaming model '" + modelName + "': " + cause.getMessage(), cause); + "Failed to initialize streaming model '" + modelName + "': " + e.getMessage(), e); Logger.warn(LangChain4jAIClient.class, - "Streaming model '" + modelName + "' init failed: " + cause.getMessage() + "Streaming model '" + modelName + "' init failed: " + e.getMessage() + (models.size() > 1 ? " — trying next model" : "")); } } @@ -345,24 +341,27 @@ String executeWithFallback( // all configured fallback models have been attempted. RuntimeException lastException = null; for (final String modelName : models) { + final ProviderConfig modelConfig = ImmutableProviderConfig.copyOf(baseConfig).withModel(modelName); + final M model; try { - final ProviderConfig modelConfig = ImmutableProviderConfig.copyOf(baseConfig).withModel(modelName); - final M model = modelCache.get( + model = modelCache.get( cacheKeyPrefix + ":" + section + ":" + modelName, - () -> modelBuilder.apply(modelConfig)); + k -> modelBuilder.apply(modelConfig)); + } catch (RuntimeException e) { + lastException = new IllegalArgumentException( + "Failed to initialize " + section + " model '" + modelName + "': " + e.getMessage(), e); + Logger.warn(LangChain4jAIClient.class, + section + " model '" + modelName + "' init failed: " + e.getMessage() + + (models.size() > 1 ? " — trying next model" : "")); + continue; + } + try { final long start = System.currentTimeMillis(); final String result = executor.apply(model); Logger.info(LangChain4jAIClient.class, section + " model '" + modelName + "' responded in " + (System.currentTimeMillis() - start) + "ms"); return result; - } catch (ExecutionException | UncheckedExecutionException e) { - final Throwable cause = e.getCause() != null ? e.getCause() : e; - lastException = new IllegalArgumentException( - "Failed to initialize " + section + " model '" + modelName + "': " + cause.getMessage(), cause); - Logger.warn(LangChain4jAIClient.class, - section + " model '" + modelName + "' init failed: " + cause.getMessage() - + (models.size() > 1 ? " — trying next model" : "")); } catch (RuntimeException e) { lastException = e; Logger.warn(LangChain4jAIClient.class, diff --git a/dotCMS/src/main/webapp/WEB-INF/jsp/dotai/render.jsp b/dotCMS/src/main/webapp/WEB-INF/jsp/dotai/render.jsp index 559637e06850..7394ab69e52c 100644 --- a/dotCMS/src/main/webapp/WEB-INF/jsp/dotai/render.jsp +++ b/dotCMS/src/main/webapp/WEB-INF/jsp/dotai/render.jsp @@ -356,7 +356,10 @@ placeholder="Image prompt"> - Size: + +
diff --git a/dotCMS/src/main/webapp/WEB-INF/messages/Language.properties b/dotCMS/src/main/webapp/WEB-INF/messages/Language.properties index 2af698fc7c78..b7c5d80ca465 100644 --- a/dotCMS/src/main/webapp/WEB-INF/messages/Language.properties +++ b/dotCMS/src/main/webapp/WEB-INF/messages/Language.properties @@ -5927,6 +5927,7 @@ block-editor.extension.ai-image.custom.prompt.desc=Write your own prompt and hav block-editor.extension.ai-image.existing.content=Create based on Existing Content block-editor.extension.ai-image.existing.content.desc=Generate and image based on provided content. You may Customize with mood, colors and size. block-editor.extension.ai-image.orientation=Orientation +block-editor.extension.ai-image.size=Size block-editor.extension.ai-image.prompt=Prompt block-editor.extension.ai-image.custom.props=Customize images Properties ( Optional ) diff --git a/dotCMS/src/test/java/com/dotcms/ai/client/langchain4j/LangChain4jAIClientTest.java b/dotCMS/src/test/java/com/dotcms/ai/client/langchain4j/LangChain4jAIClientTest.java index e35552923a74..5a1b05096758 100644 --- a/dotCMS/src/test/java/com/dotcms/ai/client/langchain4j/LangChain4jAIClientTest.java +++ b/dotCMS/src/test/java/com/dotcms/ai/client/langchain4j/LangChain4jAIClientTest.java @@ -6,8 +6,8 @@ import com.dotcms.ai.exception.DotAIAppConfigDisabledException; import com.dotmarketing.util.json.JSONArray; import com.dotmarketing.util.json.JSONObject; -import com.google.common.cache.Cache; -import com.google.common.cache.CacheBuilder; +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.Caffeine; import dev.langchain4j.data.embedding.Embedding; import dev.langchain4j.data.image.Image; import dev.langchain4j.data.message.AiMessage; @@ -282,7 +282,7 @@ private static ProviderConfig configWithModels(final String models) { } private static Cache freshCache() { - return CacheBuilder.newBuilder().build(); + return Caffeine.newBuilder().build(); } @Test