Skip to content
Open
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
28 changes: 28 additions & 0 deletions includes/API/SettingsApi.php
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,34 @@ private function sanitize_ai_settings( $ai_settings ) {
}
}

// Sanitize feature toggles
if ( isset( $ai_settings['features'] ) && is_array( $ai_settings['features'] ) ) {
$sanitized['features'] = array();
$allowed_features = [ 'ai_summaries', 'ai_search', 'ai_qa', 'ai_recommendations', 'ai_acknowledgements' ];

foreach ( $allowed_features as $feature ) {
if ( isset( $ai_settings['features'][ $feature ] ) ) {
$sanitized['features'][ $feature ] = [
'enabled' => ! empty( $ai_settings['features'][ $feature ]['enabled'] ),
];

// ai_summaries is a PRO-only feature; enforce server-side.
if ( $feature === 'ai_summaries' ) {
if ( ! wedocs_is_pro_active() ) {
$sanitized['features'][ $feature ]['enabled'] = false;
$sanitized['features'][ $feature ]['display_mode'] = 'summary';
} else {
$allowed_modes = [ 'summary', 'highlights' ];
$mode = isset( $ai_settings['features'][ $feature ]['display_mode'] )
? sanitize_text_field( $ai_settings['features'][ $feature ]['display_mode'] )
: 'summary';
$sanitized['features'][ $feature ]['display_mode'] = in_array( $mode, $allowed_modes, true ) ? $mode : 'summary';
}
}
}
}
}

return $sanitized;
}

Expand Down
238 changes: 237 additions & 1 deletion src/components/Settings/AiSettings.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,10 @@
import { __ } from '@wordpress/i18n';
import { useState, useEffect, useCallback, Fragment } from '@wordpress/element';
import apiFetch from '@wordpress/api-fetch';
import { Listbox, Transition } from '@headlessui/react';
import { Listbox, Transition, Switch } from '@headlessui/react';
import { CheckIcon, ChevronDownIcon, ArrowPathIcon } from '@heroicons/react/20/solid';
import AiImageAnalysisPreview from '../ProPreviews/AiImageAnalysisPreview';
import Overlay from '../ProPreviews/common/Overlay';

const AiSettings = ({
settingsData,
Expand Down Expand Up @@ -284,6 +285,48 @@ const AiSettings = ({
});
};

// Toggle an AI feature on/off (ai_summaries, ai_search, ai_qa, etc.).
const handleFeatureToggle = (feature, value) => {
const updatedSettings = {
...aiSettings,
features: {
...aiSettings.features,
[feature]: {
...aiSettings.features?.[feature],
enabled: value,
},
},
};

setAiSettings(updatedSettings);
setSettings({
...settingsData,
ai: updatedSettings,
});
};

const isPro = !!window.weDocsAdminVars?.pro_active;
const [showSummaryOverlay, setShowSummaryOverlay] = useState(false);

// Change the default display mode for the AI summary feature.
const handleSummaryModeChange = (mode) => {
const updatedSettings = {
...aiSettings,
features: {
...aiSettings.features,
ai_summaries: {
...aiSettings.features?.ai_summaries,
display_mode: mode,
},
},
};
setAiSettings(updatedSettings);
setSettings({ ...settingsData, ai: updatedSettings });
};

// Check if the active provider has an API key saved.
const hasApiKey =
!!aiSettings.providers[aiSettings.default_provider]?.api_key;

const maskApiKey = (key) => {
if (!key) return '';
Expand Down Expand Up @@ -756,6 +799,199 @@ const AiSettings = ({
{!wp.hooks.applyFilters('wedocs_pro_loaded', false) && (
<AiImageAnalysisPreview />
)}

{/* AI Doc Summary — PRO feature */}
<div
className="col-span-4 relative"
onMouseEnter={() => !isPro && setShowSummaryOverlay(true)}
onMouseLeave={() => setShowSummaryOverlay(false)}
>
<hr className="h-px !bg-gray-200 border-0 dark:!bg-gray-200 mb-5" />
<div className="settings-content flex items-center justify-between">
<div className="settings-field-heading md:min-w-[300px] flex items-center space-x-2 flex-1">
<label className="block text-sm font-medium text-gray-600">
{__('Enable AI Doc Summary', 'wedocs')}
</label>
<div
className="tooltip cursor-pointer ml-2 z-[9999]"
data-tip={__(
'Show a collapsible AI-generated summary on each doc page. Requires an API key.',
'wedocs'
)}
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="18"
height="18"
fill="none"
>
<path
d="M9.833 12.333H9V9h-.833M9 5.667h.008M16.5 9a7.5 7.5 0 1 1-15 0 7.5 7.5 0 1 1 15 0z"
stroke="#6b7280"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</div>
</div>
<div className="settings-field w-full max-w-[490px] ml-auto flex-2">
<div
className={`flex items-center ${
!hasApiKey ? 'opacity-50 pointer-events-none' : ''
}`}
>
<Switch
checked={!!aiSettings.features?.ai_summaries?.enabled}
onChange={(val) =>
handleFeatureToggle('ai_summaries', val)
}
disabled={!hasApiKey || !isPro}
aria-label="Toggle AI document summaries"
className="group relative inline-flex h-5 w-10 flex-shrink-0 cursor-pointer outline-0 items-center justify-center rounded-full"
>
<span
aria-hidden="true"
className="pointer-events-none absolute h-full w-full rounded-md bg-white"
/>
<span
aria-hidden="true"
className={`pointer-events-none absolute mx-auto h-4 w-9 rounded-full transition-colors duration-200 ease-in-out ${
!!aiSettings.features?.ai_summaries?.enabled
? 'bg-indigo-600'
: 'bg-gray-200'
}`}
/>
<span
aria-hidden="true"
className={`pointer-events-none absolute left-0 inline-block h-5 w-5 transform rounded-full border border-gray-200 bg-white shadow ring-0 transition-transform duration-200 ease-in-out ${
!!aiSettings.features?.ai_summaries?.enabled
? 'translate-x-5'
: 'translate-x-0'
}`}
/>
</Switch>
<span className="ml-3 text-sm text-gray-900">
{!!aiSettings.features?.ai_summaries?.enabled
? __('Enable', 'wedocs')
: __('Disable', 'wedocs')}
</span>
</div>
{!hasApiKey && isPro && (
<p className="text-xs text-amber-600 mt-1">
{__(
'Add an API key above to enable AI Doc Summary.',
'wedocs'
)}
</p>
)}
</div>
</div>
<div className="settings-description w-full max-w-[490px] ml-auto mt-1">
<p className="text-sm text-[#6B7280]">
{__(
'When enabled, readers can expand a collapsible AI summary on any doc page.',
'wedocs'
)}
</p>
</div>

{/* Default Display Mode */}
<div className="settings-content flex items-center justify-between mt-5">
<div className="settings-field-heading md:min-w-[300px] flex items-center space-x-2 flex-1">
<label className="block text-sm font-medium text-gray-600">
{__('Default Display Mode', 'wedocs')}
</label>
<div
className="tooltip cursor-pointer ml-2 z-[9999]"
data-tip={__(
'Force all docs to use a specific presentation mode.',
'wedocs'
)}
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="18"
height="18"
fill="none"
>
<path
d="M9.833 12.333H9V9h-.833M9 5.667h.008M16.5 9a7.5 7.5 0 1 1-15 0 7.5 7.5 0 1 1 15 0z"
stroke="#6b7280"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</div>
</div>
<div className="settings-field w-full max-w-[490px] ml-auto flex-2">
<div className="flex items-center gap-4">
{[
{ value: 'summary', label: __('Summary only', 'wedocs') },
{
value: 'highlights',
label: __('Highlights only', 'wedocs'),
},
].map((opt) => {
const isSelected =
(aiSettings.features?.ai_summaries?.display_mode ||
'summary') === opt.value;
return (
<label
key={opt.value}
className={`flex items-center gap-2 cursor-pointer text-sm ${
isSelected
? 'text-gray-900 font-medium'
: 'text-gray-600'
} ${
!isPro || !hasApiKey
? 'opacity-50 pointer-events-none'
: ''
}`}
>
<span
className={`relative inline-flex items-center justify-center h-4 w-4 rounded-full border-2 flex-shrink-0 transition-colors duration-150 ${
isSelected
? 'border-indigo-600'
: 'border-gray-300'
}`}
>
{isSelected && (
<span className="h-2 w-2 rounded-full bg-indigo-600" />
)}
</span>
<input
type="radio"
name="ai_summaries_display_mode"
value={opt.value}
checked={isSelected}
onChange={() => handleSummaryModeChange(opt.value)}
disabled={!isPro || !hasApiKey}
className="sr-only"
/>
{opt.label}
</label>
);
})}
</div>
<p className="text-xs text-[#6B7280] mt-1">
{__(
'Force all docs to use a specific mode if needed.',
'wedocs'
)}
</p>
</div>
</div>

<Overlay
classes={`${
showSummaryOverlay
? 'flex items-center justify-center'
: 'hidden'
}`}
/>
</div>
</div>
</div>
</div>
Expand Down
Loading