Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ describe('DotAiConfigService', () => {
spectator.service.getResolvedConfig().subscribe((r) => (result = r));

flush({
configHost: 'demo.dotcms.com (falls back to system host)',
configHost: 'demo.dotcms.com',
settings: { temperature: '0.7' },
providerConfig: JSON.stringify({
chat: { provider: 'openrouter', apiKey: '*****', model: 'a,b,c' }
Expand All @@ -134,12 +134,12 @@ describe('DotAiConfigService', () => {
spectator.service.getResolvedConfig().subscribe((r) => (result = r));

flush({
configHost: 'demo.dotcms.com (falls back to system host)',
configHost: 'demo.dotcms.com',
settings: {},
providerConfig: '{}'
});

expect(result.configHost).toBe('demo.dotcms.com (falls back to system host)');
expect(result.configHost).toBe('demo.dotcms.com');
});

it('should report isConfigured false when providerConfig is omitted', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ interface ResponseEntityView<T> {

interface RawCompletionsConfig {
configHost: string;
/** Absent on an older backend, where the hostname carried the fallback note inline. */
configHostInherited?: boolean;
settings?: Record<string, string>;
/** Omitted entirely by the backend when blank — that absence is the "not configured" signal. */
providerConfig?: string;
Expand Down Expand Up @@ -130,6 +132,7 @@ export class DotAiConfigService {
#toResolvedConfig(raw: RawCompletionsConfig): DotAiResolvedConfig {
const base = {
configHost: raw?.configHost ?? '',
configHostInherited: raw?.configHostInherited ?? false,
settings: raw?.settings ?? {}
};

Expand Down
7 changes: 6 additions & 1 deletion core-web/libs/dotcms-models/src/lib/dot-ai.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -270,8 +270,13 @@ export interface DotAiEmbeddingsBuildResult {

/** `providerConfig` parsed exactly once, by DotAiConfigService. */
export interface DotAiResolvedConfig {
/** A display string from the server, e.g. "demo.dotcms.com (falls back to system host)". */
/** The site the configuration is read for, e.g. "demo.dotcms.com". */
configHost: string;
/**
* The site has no dotAI configuration of its own, so what is shown came from the System
* Host. The server reports this separately from the hostname so the client can label it.
*/
configHostInherited: boolean;
settings: Record<string, string>;
providerConfig: Record<string, unknown> | null;
/** `chat.model` is a CSV fallback list whose first entry is the default. */
Expand Down
2 changes: 2 additions & 0 deletions core-web/libs/portlets/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ this.dialogService.open(MyFormComponent, { width: '700px', ... });

**Modal dialogs (default)**: List component opens `DialogService.open(CreateComponent, ...)`. The dialog closes with the form value; the list component passes it to the store. This is the pattern used in `dot-tags` and should be the default for new portlets.

**Dialogs whose submit can fail into the form (exception)**: when a submit can be rejected in a way that is *a correction to a field the dialog is still holding* — a Lucene or GraphQL query the server parses, say — `close(formValue)` loses the input at the moment the user needs it, and the error surfaces on the screen behind a modal that has already gone. Such a dialog keeps ownership of the submit: it calls the store itself, renders the failure inline, and closes only on success. Keep the request state local to the dialog rather than adding an in-flight flag to portlet state, and clear any unrendered outcome in the dialog's `DestroyRef.onDestroy` — PrimeNG's header X and the Escape key call `DynamicDialogRef.close` directly and never reach your own cancel handler. See `dot-ai`'s `dot-ai-index-create`. `close(formValue)` stays the default for every dialog whose submit cannot fail into the form.

**Routed CRUD (rare)**: Separate route for create/edit pages. Use only when the form is too complex for a dialog (many tabs, nested data). See `dot-experiments` for this pattern.

## When the CRUD Pattern Is Not Enough
Expand Down

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import {
DotAIImageOrientation,
DOT_AI_VECTOR_OPERATOR,
DotAiIndex,
DotAiIndexStatus,
DotAiSearchResponse,
DotAiVectorOperator
} from '@dotcms/dotcms-models';
Expand Down Expand Up @@ -47,17 +46,32 @@ export const DOT_AI_TABS = [
export type DotAiTab = (typeof DOT_AI_TABS)[number];
export type DotAiTabId = DotAiTab['id'];

export const DOT_AI_INDEX_OPERATION = {
BUILD: 'build',
REMOVE_CONTENT: 'removeContent',
DELETE_INDEX: 'deleteIndex',
REBUILD_DB: 'rebuildDb'
} as const;

export type DotAiIndexOperation =
(typeof DOT_AI_INDEX_OPERATION)[keyof typeof DOT_AI_INDEX_OPERATION];

/**
* The outcome of the last index build, surfaced in the tab.
* What the last index operation did.
*
* One channel for all four, because where an outcome belongs is decided by the outcome rather
* than the operation: anything the user has to act on goes in the dialog still holding the
* field that caused it, everything else is a toast.
*
* `empty` is its own case on purpose: the server answers 200 with `totalToEmbed: 0` when the
* query matches nothing, and an index with no rows does not come back from `indexCount` at
* all — so without this the build looks like it silently did nothing.
* `empty` is its own outcome on purpose. A build whose query matches nothing answers 200 with
* `totalToEmbed: 0`, and the index it makes never comes back from `indexCount`; a removal whose
* query matches nothing answers 200 with `deleted: 0`. Both are a query to fix, not a success.
*/
export interface DotAiIndexBuildNotice {
kind: 'built' | 'empty' | 'failed';
export interface DotAiIndexNotice {
operation: DotAiIndexOperation;
outcome: 'ok' | 'empty' | 'failed';
indexName: string;
/** Rows embedded, for `built`; the server's reason, for `failed`. */
/** The count, for `ok`; the server's reason, for `failed`. */
detail?: string;
}

Expand Down Expand Up @@ -112,16 +126,26 @@ export interface DotAiPortletState {
*/
configLoadFailed: boolean;
configHost: string;
/** The site has no dotAI configuration of its own; these settings came from System Host. */
configHostInherited: boolean;
settings: Record<string, string>;
chatModels: string[];
redactionFailed: boolean;
providerConfig: Record<string, unknown> | null;

// indexes
indexes: DotAiIndex[];
indexStatuses: Record<string, DotAiIndexStatus>;
indexFragmentSnapshot: Record<string, number>;
indexBuildSeeds: string[];
/**
* Builds that have been requested but may not be in `indexes` yet — name to the epoch ms
* the build was requested. Embedding is asynchronous, so a freshly built index has no rows
* in `dot_embeddings` and does not come back from `indexCount` for a second or two; this is
* what lets the row show as Building in the meantime instead of not showing at all.
*
* The timestamp is the stop condition: a build that never materialises would otherwise poll
* for the lifetime of the page. See `PENDING_SEED_TTL_MS`.
*/
indexBuildSeeds: Record<string, number>;
indexesForbidden: boolean;

// shared retrieval settings
Expand All @@ -148,7 +172,13 @@ export interface DotAiPortletState {

// embeddings screen (client-side filters — the whole dataset arrives in one response)
indexFilter: string;
indexBuildNotice: DotAiIndexBuildNotice | null;
indexNotice: DotAiIndexNotice | null;
/**
* The operation and index whose outcome an open dialog will render itself.
*
* Declared at submit, not at render, so nothing depends on which effect runs first.
*/
indexNoticeOwner: { operation: DotAiIndexOperation; indexName: string } | null;

// image
image: DotAiGeneratedImage | null;
Expand Down Expand Up @@ -192,15 +222,15 @@ export const DOT_AI_INITIAL_STATE: DotAiPortletState = {
configLoaded: false,
configLoadFailed: false,
configHost: '',
configHostInherited: false,
settings: {},
chatModels: [],
redactionFailed: false,
providerConfig: null,

indexes: [],
indexStatuses: {},
indexFragmentSnapshot: {},
indexBuildSeeds: [],
indexBuildSeeds: {},
indexesForbidden: false,

settingsIndexName: 'default',
Expand All @@ -222,7 +252,8 @@ export const DOT_AI_INITIAL_STATE: DotAiPortletState = {
chatStreaming: false,

indexFilter: '',
indexBuildNotice: null,
indexNotice: null,
indexNoticeOwner: null,

image: null,
imageGenerating: false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { withAiConfig } from './with-ai-config.feature';
import { DOT_AI_INITIAL_STATE, DotAiPortletState } from '../../models/dot-ai-portlet.models';

const resolved = (overrides: Partial<DotAiResolvedConfig> = {}): DotAiResolvedConfig => ({
configHost: 'demo.dotcms.com (falls back to system host)',
configHost: 'demo.dotcms.com',
settings: { embeddingsSearchThreshold: '0.4' },
providerConfig: { chat: { model: 'a,b' } },
chatModels: ['a', 'b'],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export function withAiConfig() {
/** The resolved config reassembled from state, for the Config Values screen. */
resolvedConfig: computed<DotAiResolvedConfig>(() => ({
configHost: store.configHost(),
configHostInherited: store.configHostInherited(),
settings: store.settings(),
providerConfig: store.providerConfig(),
chatModels: store.chatModels(),
Expand Down Expand Up @@ -97,6 +98,7 @@ export function withAiConfig() {
configLoadFailed: false,
isConfigured: config.isConfigured,
configHost: config.configHost,
configHostInherited: config.configHostInherited,
settings: config.settings,
chatModels: config.chatModels,
redactionFailed: config.redactionFailed,
Expand Down
Loading
Loading