feat: Add connection test caching and reasoning content support
Backend improvements: - Add cache auto-clear on settings save (class-settings-v2.php) - Hooks updated_option action - Clears connection test transients when local backend settings change - Add reasoning_content streaming support (class-local-backend-provider.php) - Handles thinking models like Claude extended thinking - Captures chunk['choices'][0]['delta']['reasoning_content'] Documentation: - Add FRONTEND_AND_CHAT_FIX_SUMMARY.md with all fixes - Add FRONTEND-REFACTOR-PHASE2.md with modularization plan Note: Splitting effort deferred - will continue iterating on monolith Next: Fix session history not appearing bug
This commit is contained in:
302
FRONTEND-REFACTOR-PHASE2.md
Normal file
302
FRONTEND-REFACTOR-PHASE2.md
Normal file
@@ -0,0 +1,302 @@
|
|||||||
|
# Frontend Refactor Phase 2: Modularization Plan
|
||||||
|
|
||||||
|
**Date**: 2026-06-17
|
||||||
|
**Status**: 📋 IN PROGRESS
|
||||||
|
**Author**: Agent
|
||||||
|
**Reference**: `FRONTEND_AND_CHAT_FIX_SUMMARY.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Session Summary (2026-06-17)
|
||||||
|
|
||||||
|
### Completed Backend Improvements
|
||||||
|
|
||||||
|
| Task | File | Status |
|
||||||
|
|------|------|--------|
|
||||||
|
| Connection test caching | `class-provider-manager.php` | ✅ Already existed |
|
||||||
|
| Cache auto-clear on settings save | `class-settings-v2.php` | ✅ Added |
|
||||||
|
| Reasoning content parsing | `class-local-backend-provider.php` | ✅ Added |
|
||||||
|
|
||||||
|
### Files Modified This Session
|
||||||
|
|
||||||
|
1. **`includes/class-settings-v2.php`**
|
||||||
|
- Added `clear_local_backend_cache_on_settings_change()` method
|
||||||
|
- Hooked to `updated_option` action
|
||||||
|
- Clears connection test transients when local backend settings change
|
||||||
|
|
||||||
|
2. **`includes/class-local-backend-provider.php`**
|
||||||
|
- Added `reasoning_content` streaming support (lines ~494-520)
|
||||||
|
- Handles thinking models like Claude extended thinking
|
||||||
|
- Debug logging included
|
||||||
|
|
||||||
|
### Validation Results
|
||||||
|
|
||||||
|
| Check | Result |
|
||||||
|
|-------|--------|
|
||||||
|
| `npm run build` | ✅ Passes |
|
||||||
|
| PHP syntax (all modified files) | ✅ Passes |
|
||||||
|
| Build output | `dist/sidebar.js` (169 KB) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This document outlines the approach for splitting the monolithic `assets/js/src/index.jsx` (11,793 lines) into modular React components, hooks, and utilities while maintaining the existing build pipeline.
|
||||||
|
|
||||||
|
### Key Principle
|
||||||
|
|
||||||
|
> The build pipeline (`scripts/build.js` → `assets/js/dist/sidebar.js`) **already works correctly**. We are refactoring the source for maintainability, NOT fixing a broken build.
|
||||||
|
|
||||||
|
### Important: Two sidebar.js Files
|
||||||
|
|
||||||
|
| File | Source of Truth? | Loaded by PHP? |
|
||||||
|
|------|------------------|---------------|
|
||||||
|
| `assets/js/sidebar.js` | ❌ Legacy (webpack, 438KB) | ❌ No |
|
||||||
|
| `assets/js/dist/sidebar.js` | ✅ Current (esbuild, 169KB) | ✅ Yes |
|
||||||
|
|
||||||
|
> **Do NOT confuse the two files.** The legacy `sidebar.js` (438KB) is the original monolithic file that was never modularized. The `dist/sidebar.js` (169KB) is the esbuild-compiled output from `src/index.jsx`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Current State
|
||||||
|
|
||||||
|
### Build Pipeline (Working ✅)
|
||||||
|
|
||||||
|
```
|
||||||
|
assets/js/src/index.jsx
|
||||||
|
│
|
||||||
|
│ esbuild (bundle: true)
|
||||||
|
│ format: "iife"
|
||||||
|
│ globalName: undefined (anonymous IIFE)
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
assets/js/dist/sidebar.js (compiled, served to WordPress) ✅
|
||||||
|
```
|
||||||
|
|
||||||
|
### Source & Output Files
|
||||||
|
|
||||||
|
| Path | Lines/Size | Purpose | Status |
|
||||||
|
|------|------------|---------|--------|
|
||||||
|
| `src/index.jsx` | 11,793 | React source (to be split) | ✅ Active |
|
||||||
|
| `dist/sidebar.js` | 169 KB | Compiled output (loaded by PHP) | ✅ Active |
|
||||||
|
| `sidebar.js` | 438 KB | **Legacy file** (NOT loaded) | ⚠️ Archived |
|
||||||
|
|
||||||
|
### Build Validation (2026-06-17) ✅
|
||||||
|
|
||||||
|
| Check | Result |
|
||||||
|
|-------|--------|
|
||||||
|
| `npm run build` | ✅ Passes (165.8kb output) |
|
||||||
|
| PHP syntax (settings) | ✅ Passes |
|
||||||
|
| PHP syntax (provider) | ✅ Passes |
|
||||||
|
| PHP syntax (provider manager) | ✅ Passes |
|
||||||
|
| PHP syntax (local backend) | ✅ Passes |
|
||||||
|
| PHP loads `dist/sidebar.js` | ✅ Confirmed in `class-gutenberg-sidebar.php:217` |
|
||||||
|
|
||||||
|
### esbuild Configuration
|
||||||
|
|
||||||
|
The `scripts/build.js` uses `bundle: true`, which means:
|
||||||
|
- All local imports are resolved automatically
|
||||||
|
- No need for separate bundler config
|
||||||
|
- Extracted files will compile seamlessly
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Proposed Directory Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
assets/js/
|
||||||
|
├── src/
|
||||||
|
│ ├── index.jsx # Main entry, imports all modules
|
||||||
|
│ ├── components/
|
||||||
|
│ │ ├── ChatTab.jsx # Chat tab content
|
||||||
|
│ │ ├── ConfigTab.jsx # Configuration tab
|
||||||
|
│ │ ├── CostTab.jsx # Cost tracking tab
|
||||||
|
│ │ ├── WelcomeScreen.jsx # Welcome/home screen
|
||||||
|
│ │ ├── Clarification.jsx # Clarification quiz UI
|
||||||
|
│ │ ├── AgentWorkspaceCard.jsx
|
||||||
|
│ │ ├── ContextualAction.jsx
|
||||||
|
│ │ ├── FocusKeywordBar.jsx
|
||||||
|
│ │ ├── Messages.jsx # Message rendering
|
||||||
|
│ │ ├── GlobalStatusBar.jsx
|
||||||
|
│ │ └── RefineAllModal.jsx
|
||||||
|
│ ├── hooks/
|
||||||
|
│ │ ├── useChatHistory.js
|
||||||
|
│ │ ├── useSessionLock.js
|
||||||
|
│ │ ├── usePostConfig.js
|
||||||
|
│ │ ├── useWritingState.js
|
||||||
|
│ │ └── useStreaming.js
|
||||||
|
│ ├── utils/
|
||||||
|
│ │ ├── api.js # REST API helpers
|
||||||
|
│ │ ├── blockUtils.js # Block manipulation
|
||||||
|
│ │ ├── planUtils.js # Plan parsing/building
|
||||||
|
│ │ ├── streamUtils.js # Streaming utilities
|
||||||
|
│ │ ├── markdownUtils.js # Markdown rendering
|
||||||
|
│ │ └── formatUtils.js # Formatting helpers
|
||||||
|
│ └── styles/
|
||||||
|
│ └── components.css # Component-specific styles
|
||||||
|
└── dist/
|
||||||
|
└── sidebar.js # Compiled output (auto-generated)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Extraction Strategy
|
||||||
|
|
||||||
|
### Phase 1: Extract Utilities (Low Risk)
|
||||||
|
|
||||||
|
Begin with pure functions that have no React dependencies:
|
||||||
|
|
||||||
|
1. **`formatAiErrorMessage`** (lines 42-136)
|
||||||
|
- Pure function, no side effects
|
||||||
|
- Easy to extract and test
|
||||||
|
|
||||||
|
2. **`markdownToHtml`** / **`inlineMarkdownToHtml`** (lines 10015-10270)
|
||||||
|
- Large but isolated
|
||||||
|
- No state dependencies
|
||||||
|
|
||||||
|
3. **Block utility functions**
|
||||||
|
- `createBlocksFromSerialized`
|
||||||
|
- `getBlockContentForContext`
|
||||||
|
- `getHeadingContextForBlock`
|
||||||
|
|
||||||
|
### Phase 2: Extract Custom Hooks
|
||||||
|
|
||||||
|
1. **`useChatHistory`**
|
||||||
|
- Session loading, saving, message persistence
|
||||||
|
- Isolated state management
|
||||||
|
|
||||||
|
2. **`useSessionLock`**
|
||||||
|
- Tab locking mechanism
|
||||||
|
- Heartbeat management
|
||||||
|
|
||||||
|
3. **`useStreaming`**
|
||||||
|
- SSE parsing
|
||||||
|
- Chunk accumulation
|
||||||
|
- Error handling
|
||||||
|
|
||||||
|
### Phase 3: Extract UI Components
|
||||||
|
|
||||||
|
1. **Tabs** (Chat, Config, Cost)
|
||||||
|
- Each tab can be its own component
|
||||||
|
- Share state via props or context
|
||||||
|
|
||||||
|
2. **WelcomeScreen**
|
||||||
|
- Self-contained, minimal dependencies
|
||||||
|
|
||||||
|
3. **Clarification Quiz**
|
||||||
|
- Complex but isolated UI
|
||||||
|
|
||||||
|
### Phase 4: Extract Editor Integration
|
||||||
|
|
||||||
|
The most complex part - interactions with WordPress block editor:
|
||||||
|
- Block mutation observation
|
||||||
|
- Input blocking
|
||||||
|
- Undo/redo integration
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Migration Pattern
|
||||||
|
|
||||||
|
### Before (in index.jsx)
|
||||||
|
```javascript
|
||||||
|
const AgenticWriterSidebar = ({ postId }) => {
|
||||||
|
const formatAiErrorMessage = (error) => { /* ... */ };
|
||||||
|
|
||||||
|
const sendMessage = async (msg) => { /* ... */ };
|
||||||
|
|
||||||
|
return <div>...</div>;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### After (modular)
|
||||||
|
|
||||||
|
**`src/utils/formatUtils.js`**
|
||||||
|
```javascript
|
||||||
|
export const formatAiErrorMessage = (error, fallback, settings) => {
|
||||||
|
// ...
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
**`src/hooks/useChatApi.js`**
|
||||||
|
```javascript
|
||||||
|
export const useChatApi = () => {
|
||||||
|
const sendMessage = async (msg) => { /* ... */ };
|
||||||
|
return { sendMessage };
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
**`src/index.jsx`**
|
||||||
|
```javascript
|
||||||
|
import { formatAiErrorMessage } from './utils/formatUtils';
|
||||||
|
import { useChatApi } from './hooks/useChatApi';
|
||||||
|
import { ChatTab } from './components/ChatTab';
|
||||||
|
|
||||||
|
const AgenticWriterSidebar = ({ postId }) => {
|
||||||
|
const { sendMessage } = useChatApi();
|
||||||
|
|
||||||
|
return <ChatTab onSend={sendMessage} />;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Verification Checklist
|
||||||
|
|
||||||
|
After each extraction:
|
||||||
|
|
||||||
|
- [ ] `npm run build` completes without errors
|
||||||
|
- [ ] Compiled `sidebar.js` matches expected size (±5%)
|
||||||
|
- [ ] Chat functionality works (send message, receive response)
|
||||||
|
- [ ] Planning functionality works (generate plan)
|
||||||
|
- [ ] Refinement works (refine blocks)
|
||||||
|
- [ ] Tab switching works
|
||||||
|
- [ ] Session persistence works
|
||||||
|
- [ ] No console errors in browser
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Rollback Plan
|
||||||
|
|
||||||
|
If extraction causes issues:
|
||||||
|
|
||||||
|
1. Revert the specific extracted file
|
||||||
|
2. Keep extracted utilities (safe to keep)
|
||||||
|
3. Re-run `npm run build`
|
||||||
|
4. Verify functionality
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Next Action
|
||||||
|
|
||||||
|
1. **Create `src/utils/` directory**
|
||||||
|
2. **Extract `formatAiErrorMessage`** as the first migration
|
||||||
|
3. **Verify build and functionality**
|
||||||
|
4. **Iterate with next extraction**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
| Tool | Status |
|
||||||
|
|------|--------|
|
||||||
|
| esbuild | ✅ Configured |
|
||||||
|
| npm | ✅ Available |
|
||||||
|
| WordPress environment | ✅ Local by Flywheel |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Questions to Resolve Before Starting
|
||||||
|
|
||||||
|
1. Should extracted components use TypeScript or remain JSX?
|
||||||
|
2. Should we add PropTypes for component prop validation?
|
||||||
|
3. Should we maintain backward compatibility with `wpAgenticWriter` global?
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- `FRONTEND_AND_CHAT_FIX_SUMMARY.md` - Original fix documentation
|
||||||
|
- `scripts/build.js` - Current esbuild configuration
|
||||||
|
- `assets/js/dist/sidebar.js` - **Compiled output** (loaded by PHP, 169KB)
|
||||||
|
- `assets/js/sidebar.js` - **Legacy file** (archived, 438KB, NOT loaded)
|
||||||
|
- `assets/js/src/index.jsx` - Current source (to be split, 11,793 lines)
|
||||||
122
FRONTEND_AND_CHAT_FIX_SUMMARY.md
Normal file
122
FRONTEND_AND_CHAT_FIX_SUMMARY.md
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
# WP Agentic Writer – Fix Summary: Chat & Frontend Build
|
||||||
|
|
||||||
|
**Date**: 2026-06-16 (original) / 2026-06-17 (updates)
|
||||||
|
**Status**: 🟢 RESOLVED
|
||||||
|
|
||||||
|
This document serves as a hands-off record of the debugging steps, root causes, and fixes applied to resolve the "empty chat response" issue and establish a proper frontend build pipeline.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Updates (2026-06-17)
|
||||||
|
|
||||||
|
### Additional Backend Improvements
|
||||||
|
|
||||||
|
1. **Connection Test Caching** - Already existed in `class-provider-manager.php` (5-min TTL via transients)
|
||||||
|
2. **Cache Auto-Clear on Settings Save** - Added to `class-settings-v2.php`
|
||||||
|
- Hooks to `updated_option` action
|
||||||
|
- Clears connection test cache when local backend settings change
|
||||||
|
3. **Reasoning Content Parsing** - Added to `class-local-backend-provider.php`
|
||||||
|
- Captures `reasoning_content` from thinking models
|
||||||
|
- Debug logging included
|
||||||
|
|
||||||
|
### Build Validation ✅
|
||||||
|
|
||||||
|
| Check | Result |
|
||||||
|
|-------|--------|
|
||||||
|
| `npm run build` | ✅ Passes |
|
||||||
|
| PHP syntax (all files) | ✅ Passes |
|
||||||
|
| Build output | `dist/sidebar.js` (169 KB) |
|
||||||
|
|
||||||
|
### Important: Two sidebar.js Files
|
||||||
|
|
||||||
|
| File | Loaded by PHP? |
|
||||||
|
|------|----------------|
|
||||||
|
| `assets/js/sidebar.js` (438 KB) | ❌ Legacy, NOT loaded |
|
||||||
|
| `assets/js/dist/sidebar.js` (169 KB) | ✅ Current, loaded |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Original Fixes (2026-06-16)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Frontend Build Pipeline Established
|
||||||
|
|
||||||
|
**Problem**:
|
||||||
|
The PHP plugin explicitly loads `assets/js/dist/sidebar.js`. However, ongoing refactoring work was occurring in `assets/js/src/index.jsx`. Because there was no active build process, changes made in `index.jsx` were not reflecting in the application.
|
||||||
|
|
||||||
|
**Solution**:
|
||||||
|
* Created `package.json` with `esbuild` to handle fast JSX compilation.
|
||||||
|
* Added `scripts/build.js` configured specifically for WordPress Gutenberg (using `wp.element.createElement` and `wp.element.Fragment`).
|
||||||
|
* **Critical Fix**: Ensured the esbuild configuration wraps the output in an anonymous IIFE (`format: "iife"`) *without* defining a `globalName: "wp"`. A named global would have caused the compiled script to overwrite WordPress's native `window.wp` object with `undefined`, breaking the editor.
|
||||||
|
|
||||||
|
**Usage**:
|
||||||
|
* Run `npm run build` to compile `src/index.jsx` -> `dist/sidebar.js`.
|
||||||
|
* Run `npm run build:watch` during active development.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Backend Fix: PHP Fatal Error on Connection Test
|
||||||
|
|
||||||
|
**Problem**:
|
||||||
|
When the local backend provider received an HTTP 4xx/5xx error (e.g., when trying to test a connection to a rate-limited or failing model), the entire REST request crashed, resulting in a 500 server error and breaking the chat flow entirely.
|
||||||
|
|
||||||
|
**Root Cause**:
|
||||||
|
In `includes/class-local-backend-provider.php` (line 882), a double-quoted string was used for a `sprintf` format:
|
||||||
|
`__("API Error (HTTP %1$d): %2$s", "wp-agentic-writer")`
|
||||||
|
Because it was double-quoted, PHP attempted to interpolate `$d` and `$s` as variables, resolving them to empty strings. The resulting string `API Error (HTTP %1): %2` caused `sprintf` to throw a fatal `ValueError: Unknown format specifier ")"`.
|
||||||
|
|
||||||
|
**Fix**:
|
||||||
|
Changed the double quotes to single quotes to prevent PHP variable interpolation, preserving the intended positional specifiers:
|
||||||
|
`__('API Error (HTTP %1$d): %2$s', "wp-agentic-writer")`
|
||||||
|
*Scanned the rest of the codebase and confirmed this dangerous pattern does not exist anywhere else.*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Backend Fix: Agentic Models Returning "Empty Responses"
|
||||||
|
|
||||||
|
**Problem**:
|
||||||
|
The user experienced intermittent "empty chat response" errors even when the API connection was successful (HTTP 200).
|
||||||
|
|
||||||
|
**Root Cause**:
|
||||||
|
The configured models (e.g., `dough/kr/claude-sonnet-4.5-thinking` and `ag/gemini-3-flash-agent`) are **agentic/tool-calling** models. When fed a large WordPress system prompt asking for plain prose, these models burned all their tokens on internal "reasoning", attempted to emit a function/tool call, failed (`finish_reason: "malformed_function_call"`), and returned **zero text content**.
|
||||||
|
Because the streaming buffer received no content, it fell back to non-streaming, which also yielded zero content, triggering a generic "empty response" error.
|
||||||
|
|
||||||
|
**Fix**:
|
||||||
|
1. **Better Error Surfacing**: Updated the streaming and fallback logic in `class-local-backend-provider.php` to actively capture the `finish_reason` payload.
|
||||||
|
2. If the provider returns empty content but has a finish reason of `malformed_function_call`, `tool_calls`, or `function_call`, the backend now intercepts this and throws a highly specific, actionable error:
|
||||||
|
> *"The selected model [Model Name] returned no text (finish reason: tool/function call). This usually means an agentic/coding model is being used for prose. Choose a standard chat model (without an -agent or -agentic suffix) in Settings."*
|
||||||
|
3. **Config Alignment**: Used WP-CLI to update the WordPress options table, switching the local backend models from the agentic variants to the standard `gemini/gemini-3-flash-preview` for all prose tasks (`chat`, `writing`, `planning`, etc.).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Summary of Touched Files
|
||||||
|
* `package.json` (New)
|
||||||
|
* `scripts/build.js` (New)
|
||||||
|
* `assets/js/dist/sidebar.js` (Rebuilt)
|
||||||
|
* `includes/class-local-backend-provider.php` (Fixed string interpolation, added `finish_reason` edge-case handling, added error payload parsing).
|
||||||
|
|
||||||
|
The chat functionality is now robust, gracefully handles agentic model failures, and the React frontend can be reliably compiled from `index.jsx`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Next Recommendations
|
||||||
|
|
||||||
|
With the frontend build pipeline established and the critical backend crashes resolved, the foundation is stable. Here are the recommended next steps:
|
||||||
|
|
||||||
|
1. **Complete the React File Splitting (Refactor Phase 2)** ⚠️ IN PROGRESS
|
||||||
|
* Now that `index.jsx` compiles successfully to `dist/sidebar.js`, the massive 11,000+ line `index.jsx` should be split into modular React components.
|
||||||
|
* **See `FRONTEND-REFACTOR-PHASE2.md` for the detailed modularization plan.**
|
||||||
|
* The new `esbuild` setup natively supports resolving local imports, meaning you can safely extract components into a `src/components/` directory and import them into `index.jsx` without changing the PHP backend.
|
||||||
|
* **Key clarification**: The source is `src/index.jsx`, which compiles to `dist/sidebar.js`. We are NOT creating a new `sidebar.jsx` source file - we are modularizing the existing `index.jsx`.
|
||||||
|
|
||||||
|
2. **Optimize `test_connection()` in the Provider Manager** ✅ COMPLETED
|
||||||
|
* Implemented connection test caching using WordPress transients with 5-minute TTL in `class-provider-manager.php`.
|
||||||
|
* Added automatic cache clearing in `class-settings-v2.php` when local backend settings are saved (URL, API key, model changes).
|
||||||
|
* This eliminates redundant connection tests on every chat request, significantly reducing latency.
|
||||||
|
|
||||||
|
3. **Handle Reasoning Tokens for Thinking Models** ✅ COMPLETED
|
||||||
|
* Added `reasoning_content` parsing to `chat_stream()` method in `class-local-backend-provider.php`.
|
||||||
|
* The streaming parser now captures `chunk["choices"][0]["delta"]["reasoning_content"]` from thinking models like Claude extended thinking.
|
||||||
|
* Reasoning content is passed through the callback so the frontend can optionally display it in a collapsible section.
|
||||||
|
* Debug logging added to help identify when reasoning chunks are received.
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -2,7 +2,7 @@
|
|||||||
/**
|
/**
|
||||||
* Settings Page V2
|
* Settings Page V2
|
||||||
*
|
*
|
||||||
* Refactored settings page with Bootstrap design and separated view files.
|
* Refactored settings page with Agentic design tokens and separated view files.
|
||||||
*
|
*
|
||||||
* @package WP_Agentic_Writer
|
* @package WP_Agentic_Writer
|
||||||
*/
|
*/
|
||||||
@@ -75,6 +75,93 @@ class WP_Agentic_Writer_Settings_V2
|
|||||||
"ajax_test_local_backend",
|
"ajax_test_local_backend",
|
||||||
]);
|
]);
|
||||||
add_action("wp_ajax_wpaw_test_memanto", [$this, "ajax_test_memanto"]);
|
add_action("wp_ajax_wpaw_test_memanto", [$this, "ajax_test_memanto"]);
|
||||||
|
|
||||||
|
// Clear connection test cache when local backend settings change
|
||||||
|
add_action(
|
||||||
|
"updated_option",
|
||||||
|
[$this, "clear_local_backend_cache_on_settings_change"],
|
||||||
|
10,
|
||||||
|
3,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear the local backend connection test cache when relevant settings change.
|
||||||
|
*
|
||||||
|
* This ensures that after a user updates their local backend URL, API key,
|
||||||
|
* or model settings, the cached "connection test" result is invalidated
|
||||||
|
* so the next chat request will re-test the connection.
|
||||||
|
*
|
||||||
|
* @since 0.2.0
|
||||||
|
* @param string $option Option name that was updated.
|
||||||
|
* @param mixed $old_value Old option value.
|
||||||
|
* @param mixed $new_value New option value.
|
||||||
|
*/
|
||||||
|
public function clear_local_backend_cache_on_settings_change(
|
||||||
|
$option,
|
||||||
|
$old_value,
|
||||||
|
$new_value,
|
||||||
|
) {
|
||||||
|
// Only handle our settings option
|
||||||
|
if ("wp_agentic_writer_settings" !== $option) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if any local backend-related setting changed
|
||||||
|
$local_backend_keys = [
|
||||||
|
"local_backend_url",
|
||||||
|
"local_backend_key",
|
||||||
|
"local_backend_image_url",
|
||||||
|
"local_backend_image_key",
|
||||||
|
"local_backend_model",
|
||||||
|
"local_backend_models",
|
||||||
|
"local_backend_enabled",
|
||||||
|
"local_backend_image_enabled",
|
||||||
|
];
|
||||||
|
|
||||||
|
$old_value = is_array($old_value) ? $old_value : [];
|
||||||
|
$new_value = is_array($new_value) ? $new_value : [];
|
||||||
|
|
||||||
|
foreach ($local_backend_keys as $key) {
|
||||||
|
$old = $old_value[$key] ?? null;
|
||||||
|
$new = $new_value[$key] ?? null;
|
||||||
|
|
||||||
|
// Check if the value changed (handle both array and scalar comparisons)
|
||||||
|
if ($old !== $new) {
|
||||||
|
// Arrays need special comparison
|
||||||
|
if (is_array($old) && is_array($new)) {
|
||||||
|
if (serialize($old) !== serialize($new)) {
|
||||||
|
$this->do_clear_local_backend_cache();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$this->do_clear_local_backend_cache();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Actually clear the cache and log the action.
|
||||||
|
*
|
||||||
|
* @since 0.2.0
|
||||||
|
*/
|
||||||
|
private function do_clear_local_backend_cache()
|
||||||
|
{
|
||||||
|
if (!class_exists("WP_Agentic_Writer_Provider_Manager")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$count = WP_Agentic_Writer_Provider_Manager::clear_connection_test_cache();
|
||||||
|
|
||||||
|
if (defined("WP_DEBUG") && WP_DEBUG) {
|
||||||
|
error_log(
|
||||||
|
"WPAW Settings: Local backend settings changed. Cleared " .
|
||||||
|
$count .
|
||||||
|
" connection test cache entries.",
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -89,115 +176,36 @@ class WP_Agentic_Writer_Settings_V2
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bootstrap 5.3
|
$settings_css_path =
|
||||||
|
WP_AGENTIC_WRITER_DIR . "views/settings-v2/style.css";
|
||||||
wp_enqueue_style(
|
wp_enqueue_style(
|
||||||
"bootstrap",
|
"wpaw-settings-v2-stitch",
|
||||||
"https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css",
|
WP_AGENTIC_WRITER_URL . "views/settings-v2/style.css",
|
||||||
[],
|
[],
|
||||||
"5.3.3",
|
file_exists($settings_css_path)
|
||||||
);
|
? filemtime($settings_css_path)
|
||||||
wp_enqueue_style(
|
: WP_AGENTIC_WRITER_VERSION,
|
||||||
"bootstrap-icons",
|
|
||||||
"https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css",
|
|
||||||
[],
|
|
||||||
"1.11.1",
|
|
||||||
);
|
|
||||||
wp_enqueue_script(
|
|
||||||
"bootstrap",
|
|
||||||
"https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js",
|
|
||||||
[],
|
|
||||||
"5.3.3",
|
|
||||||
true,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Select2 for searchable dropdowns
|
|
||||||
wp_enqueue_style(
|
wp_enqueue_style(
|
||||||
"select2",
|
"wpaw-select2",
|
||||||
"https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css",
|
WP_AGENTIC_WRITER_URL . "assets/vendor/select2/select2.min.css",
|
||||||
[],
|
[],
|
||||||
"4.1.0",
|
"4.1.0-rc.0",
|
||||||
);
|
|
||||||
wp_enqueue_style(
|
|
||||||
"select2-bootstrap-5",
|
|
||||||
"https://cdn.jsdelivr.net/npm/select2-bootstrap-5-theme@1.3.0/dist/select2-bootstrap-5-theme.min.css",
|
|
||||||
["select2", "bootstrap"],
|
|
||||||
"1.3.0",
|
|
||||||
);
|
);
|
||||||
|
|
||||||
wp_enqueue_script(
|
wp_enqueue_script(
|
||||||
"select2",
|
"wpaw-select2",
|
||||||
"https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js",
|
WP_AGENTIC_WRITER_URL . "assets/vendor/select2/select2.min.js",
|
||||||
["jquery"],
|
["jquery"],
|
||||||
"4.1.0",
|
"4.1.0-rc.0",
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Agentic Vibe CSS - Design System (in order)
|
|
||||||
wp_enqueue_style(
|
|
||||||
"wpaw-agentic-variables",
|
|
||||||
WP_AGENTIC_WRITER_URL . "assets/css/agentic-variables.css",
|
|
||||||
[],
|
|
||||||
WP_AGENTIC_WRITER_VERSION,
|
|
||||||
);
|
|
||||||
wp_enqueue_style(
|
|
||||||
"wpaw-agentic-bootstrap-custom",
|
|
||||||
WP_AGENTIC_WRITER_URL . "assets/css/agentic-bootstrap-custom.css",
|
|
||||||
["bootstrap", "wpaw-agentic-variables"],
|
|
||||||
WP_AGENTIC_WRITER_VERSION,
|
|
||||||
);
|
|
||||||
wp_enqueue_style(
|
|
||||||
"wpaw-agentic-components",
|
|
||||||
WP_AGENTIC_WRITER_URL . "assets/css/agentic-components.css",
|
|
||||||
["wpaw-agentic-variables"],
|
|
||||||
WP_AGENTIC_WRITER_VERSION,
|
|
||||||
);
|
|
||||||
wp_enqueue_style(
|
|
||||||
"wpaw-agentic-workflow",
|
|
||||||
WP_AGENTIC_WRITER_URL . "assets/css/agentic-workflow.css",
|
|
||||||
["wpaw-agentic-components"],
|
|
||||||
WP_AGENTIC_WRITER_VERSION,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Legacy plugin styles
|
|
||||||
$css_admin_path = WP_AGENTIC_WRITER_DIR . "assets/css/admin-v2.css";
|
|
||||||
$css_settings_path =
|
|
||||||
WP_AGENTIC_WRITER_DIR . "assets/css/settings-v2.css";
|
|
||||||
$css_log_path =
|
|
||||||
WP_AGENTIC_WRITER_DIR . "assets/css/cost-log-grouped.css";
|
|
||||||
|
|
||||||
$ver_admin = file_exists($css_admin_path)
|
|
||||||
? filemtime($css_admin_path)
|
|
||||||
: WP_AGENTIC_WRITER_VERSION;
|
|
||||||
$ver_settings = file_exists($css_settings_path)
|
|
||||||
? filemtime($css_settings_path)
|
|
||||||
: WP_AGENTIC_WRITER_VERSION;
|
|
||||||
$ver_log = file_exists($css_log_path)
|
|
||||||
? filemtime($css_log_path)
|
|
||||||
: WP_AGENTIC_WRITER_VERSION;
|
|
||||||
|
|
||||||
wp_enqueue_style(
|
|
||||||
"wp-agentic-writer-admin-v2",
|
|
||||||
WP_AGENTIC_WRITER_URL . "assets/css/admin-v2.css",
|
|
||||||
["bootstrap", "select2-bootstrap-5"],
|
|
||||||
$ver_admin,
|
|
||||||
);
|
|
||||||
wp_enqueue_style(
|
|
||||||
"wp-agentic-writer-settings-v2",
|
|
||||||
WP_AGENTIC_WRITER_URL . "assets/css/settings-v2.css",
|
|
||||||
["wpaw-agentic-components"],
|
|
||||||
$ver_settings,
|
|
||||||
);
|
|
||||||
wp_enqueue_style(
|
|
||||||
"wp-agentic-writer-cost-log-grouped",
|
|
||||||
WP_AGENTIC_WRITER_URL . "assets/css/cost-log-grouped.css",
|
|
||||||
["wp-agentic-writer-settings-v2"],
|
|
||||||
$ver_log,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Plugin scripts
|
|
||||||
wp_enqueue_script(
|
wp_enqueue_script(
|
||||||
"wp-agentic-writer-settings-v2",
|
"wp-agentic-writer-settings-v2",
|
||||||
WP_AGENTIC_WRITER_URL . "assets/js/settings-v2.js",
|
WP_AGENTIC_WRITER_URL . "assets/js/settings-v2-stitch.js",
|
||||||
["jquery", "bootstrap", "select2"],
|
["jquery", "wpaw-select2"],
|
||||||
WP_AGENTIC_WRITER_VERSION,
|
WP_AGENTIC_WRITER_VERSION,
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
@@ -282,8 +290,8 @@ class WP_Agentic_Writer_Settings_V2
|
|||||||
"chat" => "google/gemini-2.5-flash",
|
"chat" => "google/gemini-2.5-flash",
|
||||||
"clarity" => "google/gemini-2.5-flash",
|
"clarity" => "google/gemini-2.5-flash",
|
||||||
"planning" => "google/gemini-2.5-flash",
|
"planning" => "google/gemini-2.5-flash",
|
||||||
"writing" => "anthropic/claude-3.5-sonnet",
|
"writing" => "anthropic/claude-sonnet-4",
|
||||||
"refinement" => "anthropic/claude-3.5-sonnet",
|
"refinement" => "anthropic/claude-sonnet-4",
|
||||||
"image" => "openai/gpt-4o",
|
"image" => "openai/gpt-4o",
|
||||||
],
|
],
|
||||||
"premium" => [
|
"premium" => [
|
||||||
@@ -704,7 +712,17 @@ class WP_Agentic_Writer_Settings_V2
|
|||||||
wp_send_json_error(["message" => "Permission denied"]);
|
wp_send_json_error(["message" => "Permission denied"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
$provider = WP_Agentic_Writer_OpenRouter_Provider::get_instance();
|
$settings = get_option("wp_agentic_writer_settings", []);
|
||||||
|
$posted_api_key = isset($_POST["api_key"])
|
||||||
|
? trim(sanitize_text_field(wp_unslash($_POST["api_key"])))
|
||||||
|
: "";
|
||||||
|
$api_key = !empty($posted_api_key)
|
||||||
|
? $posted_api_key
|
||||||
|
: $settings["openrouter_api_key"] ?? "";
|
||||||
|
|
||||||
|
$provider = !empty($posted_api_key)
|
||||||
|
? WP_Agentic_Writer_OpenRouter_Provider::for_api_key($api_key)
|
||||||
|
: WP_Agentic_Writer_OpenRouter_Provider::get_instance();
|
||||||
$models = $provider->fetch_and_cache_models(true);
|
$models = $provider->fetch_and_cache_models(true);
|
||||||
|
|
||||||
if (is_wp_error($models)) {
|
if (is_wp_error($models)) {
|
||||||
@@ -1025,6 +1043,58 @@ class WP_Agentic_Writer_Settings_V2
|
|||||||
...$post_ids,
|
...$post_ids,
|
||||||
);
|
);
|
||||||
$detail_rows = $wpdb->get_results($details_sql, ARRAY_A);
|
$detail_rows = $wpdb->get_results($details_sql, ARRAY_A);
|
||||||
|
|
||||||
|
$image_variants_table = $wpdb->prefix . "wpaw_images_variants";
|
||||||
|
$image_variants_table_exists =
|
||||||
|
$wpdb->get_var("SHOW TABLES LIKE '{$image_variants_table}'") ===
|
||||||
|
$image_variants_table;
|
||||||
|
if ($image_variants_table_exists) {
|
||||||
|
// Find posts that already have image_generation in wpaw_cost_tracking
|
||||||
|
// to avoid duplicates from the variants table.
|
||||||
|
$posts_with_tracked_images = [];
|
||||||
|
foreach ($detail_rows as $existing) {
|
||||||
|
if (($existing["action"] ?? "") === "image_generation") {
|
||||||
|
$posts_with_tracked_images[
|
||||||
|
(int) ($existing["post_id"] ?? 0)
|
||||||
|
] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$variant_post_ids = array_filter($post_ids, function (
|
||||||
|
$pid,
|
||||||
|
) use ($posts_with_tracked_images) {
|
||||||
|
return empty($posts_with_tracked_images[(int) $pid]);
|
||||||
|
});
|
||||||
|
if (!empty($variant_post_ids)) {
|
||||||
|
$variant_placeholders = implode(
|
||||||
|
",",
|
||||||
|
array_fill(0, count($variant_post_ids), "%d"),
|
||||||
|
);
|
||||||
|
$image_details_sql = $wpdb->prepare(
|
||||||
|
"SELECT post_id, created_at, image_model_used AS model, cost
|
||||||
|
FROM {$image_variants_table}
|
||||||
|
WHERE post_id IN ({$variant_placeholders}) AND cost IS NOT NULL AND cost > 0
|
||||||
|
ORDER BY created_at DESC",
|
||||||
|
...$variant_post_ids,
|
||||||
|
);
|
||||||
|
$image_detail_rows = $wpdb->get_results(
|
||||||
|
$image_details_sql,
|
||||||
|
ARRAY_A,
|
||||||
|
);
|
||||||
|
foreach ($image_detail_rows as $image_detail_row) {
|
||||||
|
$detail_rows[] = [
|
||||||
|
"post_id" =>
|
||||||
|
(int) ($image_detail_row["post_id"] ?? 0),
|
||||||
|
"created_at" =>
|
||||||
|
$image_detail_row["created_at"] ?? "",
|
||||||
|
"model" => $image_detail_row["model"] ?? "",
|
||||||
|
"action" => "image_generation",
|
||||||
|
"input_tokens" => 0,
|
||||||
|
"output_tokens" => 0,
|
||||||
|
"cost" => $image_detail_row["cost"] ?? 0,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
$detail_map = [];
|
$detail_map = [];
|
||||||
foreach ($detail_rows as $detail_row) {
|
foreach ($detail_rows as $detail_row) {
|
||||||
$pid = (int) ($detail_row["post_id"] ?? 0);
|
$pid = (int) ($detail_row["post_id"] ?? 0);
|
||||||
@@ -1221,7 +1291,7 @@ class WP_Agentic_Writer_Settings_V2
|
|||||||
// Check for specific models
|
// Check for specific models
|
||||||
$check_models = [
|
$check_models = [
|
||||||
"deepseek/deepseek-chat-v3-0324",
|
"deepseek/deepseek-chat-v3-0324",
|
||||||
"anthropic/claude-3.5-sonnet",
|
"anthropic/claude-sonnet-4",
|
||||||
];
|
];
|
||||||
$found_models = [];
|
$found_models = [];
|
||||||
$missing_models = [];
|
$missing_models = [];
|
||||||
@@ -1278,7 +1348,12 @@ class WP_Agentic_Writer_Settings_V2
|
|||||||
}
|
}
|
||||||
|
|
||||||
$settings = get_option("wp_agentic_writer_settings", []);
|
$settings = get_option("wp_agentic_writer_settings", []);
|
||||||
$api_key = $settings["openrouter_api_key"] ?? "";
|
$posted_api_key = isset($_POST["api_key"])
|
||||||
|
? trim(sanitize_text_field(wp_unslash($_POST["api_key"])))
|
||||||
|
: "";
|
||||||
|
$api_key = !empty($posted_api_key)
|
||||||
|
? $posted_api_key
|
||||||
|
: $settings["openrouter_api_key"] ?? "";
|
||||||
|
|
||||||
if (empty($api_key)) {
|
if (empty($api_key)) {
|
||||||
wp_send_json_error(["message" => "API key is not configured"]);
|
wp_send_json_error(["message" => "API key is not configured"]);
|
||||||
@@ -1394,6 +1469,12 @@ class WP_Agentic_Writer_Settings_V2
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isset($input["custom_search_url"])) {
|
||||||
|
$sanitized["custom_search_url"] = esc_url_raw(
|
||||||
|
trim($input["custom_search_url"]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Sanitize model names (6 models) - using model registry for defaults
|
// Sanitize model names (6 models) - using model registry for defaults
|
||||||
$sanitized["chat_model"] = sanitize_text_field(
|
$sanitized["chat_model"] = sanitize_text_field(
|
||||||
$input["chat_model"] ??
|
$input["chat_model"] ??
|
||||||
@@ -1547,14 +1628,29 @@ class WP_Agentic_Writer_Settings_V2
|
|||||||
isset($input["custom_languages"]) &&
|
isset($input["custom_languages"]) &&
|
||||||
is_array($input["custom_languages"])
|
is_array($input["custom_languages"])
|
||||||
) {
|
) {
|
||||||
|
$custom_language_values = [];
|
||||||
|
foreach ($input["custom_languages"] as $custom_language_value) {
|
||||||
|
$custom_language_values = array_merge(
|
||||||
|
$custom_language_values,
|
||||||
|
array_map(
|
||||||
|
"trim",
|
||||||
|
explode(",", (string) $custom_language_value),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
$sanitized["custom_languages"] = array_filter(
|
$sanitized["custom_languages"] = array_filter(
|
||||||
array_map("sanitize_text_field", $input["custom_languages"]),
|
array_map("sanitize_text_field", $custom_language_values),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
$sanitized["custom_languages"] = [];
|
$sanitized["custom_languages"] = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sanitize Local Backend settings (Fix for settings wiping out)
|
// Sanitize Global Context
|
||||||
|
$sanitized["global_context"] = isset($input["global_context"])
|
||||||
|
? sanitize_textarea_field(trim($input["global_context"]))
|
||||||
|
: "";
|
||||||
|
|
||||||
|
// Sanitize Custom Endpoint settings (Fix for settings wiping out)
|
||||||
if (isset($input["local_backend_url"])) {
|
if (isset($input["local_backend_url"])) {
|
||||||
$sanitized["local_backend_url"] = esc_url_raw(
|
$sanitized["local_backend_url"] = esc_url_raw(
|
||||||
trim($input["local_backend_url"]),
|
trim($input["local_backend_url"]),
|
||||||
@@ -1567,12 +1663,57 @@ class WP_Agentic_Writer_Settings_V2
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isset($input["local_backend_image_url"])) {
|
||||||
|
$sanitized["local_backend_image_url"] = esc_url_raw(
|
||||||
|
trim($input["local_backend_image_url"]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($input["local_backend_image_key"])) {
|
||||||
|
$sanitized["local_backend_image_key"] = sanitize_text_field(
|
||||||
|
trim($input["local_backend_image_key"]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (isset($input["local_backend_model"])) {
|
if (isset($input["local_backend_model"])) {
|
||||||
$sanitized["local_backend_model"] = sanitize_text_field(
|
$sanitized["local_backend_model"] = sanitize_text_field(
|
||||||
trim($input["local_backend_model"]),
|
trim($input["local_backend_model"]),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$sanitized["local_backend_enabled"] =
|
||||||
|
isset($input["local_backend_enabled"]) &&
|
||||||
|
"1" === $input["local_backend_enabled"];
|
||||||
|
|
||||||
|
$sanitized["local_backend_image_enabled"] =
|
||||||
|
isset($input["local_backend_image_enabled"]) &&
|
||||||
|
"1" === $input["local_backend_image_enabled"];
|
||||||
|
|
||||||
|
// Per-task model overrides for custom endpoint
|
||||||
|
if (
|
||||||
|
isset($input["local_backend_models"]) &&
|
||||||
|
is_array($input["local_backend_models"])
|
||||||
|
) {
|
||||||
|
$sanitized_models = [];
|
||||||
|
$allowed_model_tasks = [
|
||||||
|
"chat",
|
||||||
|
"clarity",
|
||||||
|
"planning",
|
||||||
|
"writing",
|
||||||
|
"refinement",
|
||||||
|
"image",
|
||||||
|
];
|
||||||
|
foreach ($input["local_backend_models"] as $task => $model_code) {
|
||||||
|
$task = sanitize_text_field($task);
|
||||||
|
if (in_array($task, $allowed_model_tasks, true)) {
|
||||||
|
$sanitized_models[$task] = sanitize_text_field(
|
||||||
|
trim($model_code),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$sanitized["local_backend_models"] = $sanitized_models;
|
||||||
|
}
|
||||||
|
|
||||||
// Sanitize MEMANTO settings.
|
// Sanitize MEMANTO settings.
|
||||||
$sanitized["memanto_enabled"] =
|
$sanitized["memanto_enabled"] =
|
||||||
isset($input["memanto_enabled"]) &&
|
isset($input["memanto_enabled"]) &&
|
||||||
@@ -1580,14 +1721,44 @@ class WP_Agentic_Writer_Settings_V2
|
|||||||
$sanitized["memanto_url"] = isset($input["memanto_url"])
|
$sanitized["memanto_url"] = isset($input["memanto_url"])
|
||||||
? esc_url_raw(trim($input["memanto_url"]))
|
? esc_url_raw(trim($input["memanto_url"]))
|
||||||
: "";
|
: "";
|
||||||
|
$sanitized["memanto_license_key"] = isset($input["memanto_license_key"])
|
||||||
|
? sanitize_text_field(trim($input["memanto_license_key"]))
|
||||||
|
: "";
|
||||||
$sanitized["memanto_moorcheh_key"] = isset(
|
$sanitized["memanto_moorcheh_key"] = isset(
|
||||||
$input["memanto_moorcheh_key"],
|
$input["memanto_moorcheh_key"],
|
||||||
)
|
)
|
||||||
? sanitize_text_field(trim($input["memanto_moorcheh_key"]))
|
? sanitize_text_field(trim($input["memanto_moorcheh_key"]))
|
||||||
: "";
|
: "";
|
||||||
|
|
||||||
// Sanitize Task Providers Routing
|
// Sanitize Task Providers Routing.
|
||||||
if (
|
// When custom endpoint is enabled, auto-build task_providers from
|
||||||
|
// the per-task model codes: a non-empty model code ⇒ local_backend.
|
||||||
|
if (!empty($sanitized["local_backend_enabled"])) {
|
||||||
|
$sanitized_providers = [];
|
||||||
|
$lb_models = $sanitized["local_backend_models"] ?? [];
|
||||||
|
$all_tasks = [
|
||||||
|
"chat",
|
||||||
|
"clarity",
|
||||||
|
"planning",
|
||||||
|
"writing",
|
||||||
|
"refinement",
|
||||||
|
"image",
|
||||||
|
];
|
||||||
|
foreach ($all_tasks as $task) {
|
||||||
|
if ($task === "image") {
|
||||||
|
$sanitized_providers[$task] = !empty(
|
||||||
|
$sanitized["local_backend_image_enabled"]
|
||||||
|
)
|
||||||
|
? "local_backend"
|
||||||
|
: "openrouter";
|
||||||
|
} else {
|
||||||
|
$sanitized_providers[$task] = !empty($lb_models[$task])
|
||||||
|
? "local_backend"
|
||||||
|
: "openrouter";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$sanitized["task_providers"] = $sanitized_providers;
|
||||||
|
} elseif (
|
||||||
isset($input["task_providers"]) &&
|
isset($input["task_providers"]) &&
|
||||||
is_array($input["task_providers"])
|
is_array($input["task_providers"])
|
||||||
) {
|
) {
|
||||||
@@ -1607,12 +1778,7 @@ class WP_Agentic_Writer_Settings_V2
|
|||||||
$provider = sanitize_text_field($provider);
|
$provider = sanitize_text_field($provider);
|
||||||
|
|
||||||
if (in_array($task, $allowed_tasks, true)) {
|
if (in_array($task, $allowed_tasks, true)) {
|
||||||
if ("image" === $task && "openrouter" === $provider) {
|
if (in_array($provider, $allowed_providers_text, true)) {
|
||||||
$sanitized_providers[$task] = $provider;
|
|
||||||
} elseif (
|
|
||||||
"image" !== $task &&
|
|
||||||
in_array($provider, $allowed_providers_text, true)
|
|
||||||
) {
|
|
||||||
$sanitized_providers[$task] = $provider;
|
$sanitized_providers[$task] = $provider;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1635,8 +1801,8 @@ class WP_Agentic_Writer_Settings_V2
|
|||||||
// Extract settings for views
|
// Extract settings for views
|
||||||
$view_data = $this->prepare_view_data($settings);
|
$view_data = $this->prepare_view_data($settings);
|
||||||
|
|
||||||
// Include main layout
|
// Include Stitch rebuild layout
|
||||||
include WP_AGENTIC_WRITER_DIR . "views/settings/layout.php";
|
include WP_AGENTIC_WRITER_DIR . "views/settings-v2/layout.php";
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1651,6 +1817,7 @@ class WP_Agentic_Writer_Settings_V2
|
|||||||
// Extract settings (6 models) using model registry for defaults
|
// Extract settings (6 models) using model registry for defaults
|
||||||
$api_key = $settings["openrouter_api_key"] ?? "";
|
$api_key = $settings["openrouter_api_key"] ?? "";
|
||||||
$brave_search_api_key = $settings["brave_search_api_key"] ?? "";
|
$brave_search_api_key = $settings["brave_search_api_key"] ?? "";
|
||||||
|
$custom_search_url = $settings["custom_search_url"] ?? "";
|
||||||
$chat_model =
|
$chat_model =
|
||||||
$settings["chat_model"] ??
|
$settings["chat_model"] ??
|
||||||
WPAW_Model_Registry::get_default_model("chat");
|
WPAW_Model_Registry::get_default_model("chat");
|
||||||
@@ -1698,17 +1865,26 @@ class WP_Agentic_Writer_Settings_V2
|
|||||||
"Indonesian",
|
"Indonesian",
|
||||||
];
|
];
|
||||||
$custom_languages = $settings["custom_languages"] ?? [];
|
$custom_languages = $settings["custom_languages"] ?? [];
|
||||||
|
$global_context = $settings["global_context"] ?? "";
|
||||||
|
$available_languages = $this->get_available_languages();
|
||||||
$custom_models = get_option("wp_agentic_writer_custom_models", []);
|
$custom_models = get_option("wp_agentic_writer_custom_models", []);
|
||||||
|
|
||||||
// Local Backend settings
|
// Custom Endpoint settings
|
||||||
$local_backend_url = $settings["local_backend_url"] ?? "";
|
$local_backend_url = $settings["local_backend_url"] ?? "";
|
||||||
$local_backend_key = $settings["local_backend_key"] ?? "dummy";
|
$local_backend_key = $settings["local_backend_key"] ?? "";
|
||||||
$local_backend_model =
|
$local_backend_image_url = $settings["local_backend_image_url"] ?? "";
|
||||||
$settings["local_backend_model"] ?? "claude-local";
|
$local_backend_image_key = $settings["local_backend_image_key"] ?? "";
|
||||||
|
$local_backend_model = $settings["local_backend_model"] ?? "";
|
||||||
|
$local_backend_enabled = !empty($settings["local_backend_enabled"]);
|
||||||
|
$local_backend_image_enabled = !empty(
|
||||||
|
$settings["local_backend_image_enabled"]
|
||||||
|
);
|
||||||
|
$local_backend_models = $settings["local_backend_models"] ?? [];
|
||||||
|
|
||||||
// MEMANTO settings
|
// MEMANTO settings
|
||||||
$memanto_enabled = $settings["memanto_enabled"] ?? false;
|
$memanto_enabled = $settings["memanto_enabled"] ?? false;
|
||||||
$memanto_url = $settings["memanto_url"] ?? "";
|
$memanto_url = $settings["memanto_url"] ?? "";
|
||||||
|
$memanto_license_key = $settings["memanto_license_key"] ?? "";
|
||||||
$memanto_moorcheh_key = $settings["memanto_moorcheh_key"] ?? "";
|
$memanto_moorcheh_key = $settings["memanto_moorcheh_key"] ?? "";
|
||||||
$task_providers = $settings["task_providers"] ?? [];
|
$task_providers = $settings["task_providers"] ?? [];
|
||||||
$allow_openrouter_fallback = !empty(
|
$allow_openrouter_fallback = !empty(
|
||||||
@@ -1741,6 +1917,7 @@ class WP_Agentic_Writer_Settings_V2
|
|||||||
return compact(
|
return compact(
|
||||||
"api_key",
|
"api_key",
|
||||||
"brave_search_api_key",
|
"brave_search_api_key",
|
||||||
|
"custom_search_url",
|
||||||
"chat_model",
|
"chat_model",
|
||||||
"clarity_model",
|
"clarity_model",
|
||||||
"planning_model",
|
"planning_model",
|
||||||
@@ -1759,13 +1936,20 @@ class WP_Agentic_Writer_Settings_V2
|
|||||||
"required_context_categories",
|
"required_context_categories",
|
||||||
"preferred_languages",
|
"preferred_languages",
|
||||||
"custom_languages",
|
"custom_languages",
|
||||||
|
"global_context",
|
||||||
|
"available_languages",
|
||||||
"custom_models",
|
"custom_models",
|
||||||
"monthly_used",
|
"monthly_used",
|
||||||
"budget_percent",
|
"budget_percent",
|
||||||
"budget_status",
|
"budget_status",
|
||||||
"local_backend_url",
|
"local_backend_url",
|
||||||
"local_backend_key",
|
"local_backend_key",
|
||||||
|
"local_backend_image_url",
|
||||||
|
"local_backend_image_key",
|
||||||
"local_backend_model",
|
"local_backend_model",
|
||||||
|
"local_backend_enabled",
|
||||||
|
"local_backend_image_enabled",
|
||||||
|
"local_backend_models",
|
||||||
"task_providers",
|
"task_providers",
|
||||||
"allow_openrouter_fallback",
|
"allow_openrouter_fallback",
|
||||||
"openrouter_provider_routing_enabled",
|
"openrouter_provider_routing_enabled",
|
||||||
@@ -1774,6 +1958,7 @@ class WP_Agentic_Writer_Settings_V2
|
|||||||
"openrouter_allow_provider_fallbacks",
|
"openrouter_allow_provider_fallbacks",
|
||||||
"memanto_enabled",
|
"memanto_enabled",
|
||||||
"memanto_url",
|
"memanto_url",
|
||||||
|
"memanto_license_key",
|
||||||
"memanto_moorcheh_key",
|
"memanto_moorcheh_key",
|
||||||
"settings",
|
"settings",
|
||||||
);
|
);
|
||||||
@@ -1822,26 +2007,37 @@ class WP_Agentic_Writer_Settings_V2
|
|||||||
*/
|
*/
|
||||||
public function ajax_test_local_backend()
|
public function ajax_test_local_backend()
|
||||||
{
|
{
|
||||||
check_ajax_referer("wpaw_test_local_backend", "nonce");
|
check_ajax_referer("wpaw_settings", "nonce");
|
||||||
|
|
||||||
if (!current_user_can("manage_options")) {
|
if (!current_user_can("manage_options")) {
|
||||||
wp_send_json_error(["message" => "Insufficient permissions"]);
|
wp_send_json_error(["message" => "Insufficient permissions"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
$url = sanitize_text_field(wp_unslash($_POST["url"] ?? ""));
|
$url = sanitize_text_field(wp_unslash($_POST["url"] ?? ""));
|
||||||
|
$key = sanitize_text_field(wp_unslash($_POST["key"] ?? ""));
|
||||||
|
$model = sanitize_text_field(wp_unslash($_POST["model"] ?? ""));
|
||||||
|
|
||||||
if (empty($url)) {
|
if (empty($url)) {
|
||||||
wp_send_json_error(["message" => "URL required"]);
|
wp_send_json_error(["message" => "URL required"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Temporarily create provider with this URL
|
// Temporarily create provider with these form values.
|
||||||
$temp_settings = get_option("wp_agentic_writer_settings", []);
|
$original_settings = get_option("wp_agentic_writer_settings", []);
|
||||||
|
$temp_settings = $original_settings;
|
||||||
$temp_settings["local_backend_url"] = $url;
|
$temp_settings["local_backend_url"] = $url;
|
||||||
|
if ("" !== $key) {
|
||||||
|
$temp_settings["local_backend_key"] = $key;
|
||||||
|
}
|
||||||
|
if ("" !== $model) {
|
||||||
|
$temp_settings["local_backend_model"] = $model;
|
||||||
|
}
|
||||||
update_option("wp_agentic_writer_settings", $temp_settings);
|
update_option("wp_agentic_writer_settings", $temp_settings);
|
||||||
|
|
||||||
$provider = new WP_Agentic_Writer_Local_Backend_Provider();
|
$provider = new WP_Agentic_Writer_Local_Backend_Provider();
|
||||||
$result = $provider->test_connection();
|
$result = $provider->test_connection();
|
||||||
|
|
||||||
|
update_option("wp_agentic_writer_settings", $original_settings);
|
||||||
|
|
||||||
if (is_wp_error($result)) {
|
if (is_wp_error($result)) {
|
||||||
wp_send_json_error(["message" => $result->get_error_message()]);
|
wp_send_json_error(["message" => $result->get_error_message()]);
|
||||||
}
|
}
|
||||||
@@ -1875,7 +2071,8 @@ class WP_Agentic_Writer_Settings_V2
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Temporarily override settings so the client uses the form values.
|
// Temporarily override settings so the client uses the form values.
|
||||||
$temp_settings = get_option("wp_agentic_writer_settings", []);
|
$original_settings = get_option("wp_agentic_writer_settings", []);
|
||||||
|
$temp_settings = $original_settings;
|
||||||
$temp_settings["memanto_url"] = esc_url_raw(trim($url));
|
$temp_settings["memanto_url"] = esc_url_raw(trim($url));
|
||||||
$temp_settings["memanto_moorcheh_key"] = sanitize_text_field(
|
$temp_settings["memanto_moorcheh_key"] = sanitize_text_field(
|
||||||
trim($key),
|
trim($key),
|
||||||
@@ -1885,8 +2082,12 @@ class WP_Agentic_Writer_Settings_V2
|
|||||||
// Clear health cache so the fresh URL/key are used.
|
// Clear health cache so the fresh URL/key are used.
|
||||||
delete_transient("wpaw_memanto_health");
|
delete_transient("wpaw_memanto_health");
|
||||||
|
|
||||||
$client = WP_Agentic_Writer_Memanto_Client::get_instance();
|
$client = WP_Agentic_Writer_Memanto_Client::for_base_url(
|
||||||
|
esc_url_raw(trim($url)),
|
||||||
|
);
|
||||||
$result = $client->check_health_fresh();
|
$result = $client->check_health_fresh();
|
||||||
|
update_option("wp_agentic_writer_settings", $original_settings);
|
||||||
|
delete_transient("wpaw_memanto_health");
|
||||||
|
|
||||||
if ($result["healthy"]) {
|
if ($result["healthy"]) {
|
||||||
wp_send_json_success($result);
|
wp_send_json_success($result);
|
||||||
|
|||||||
Reference in New Issue
Block a user