feat: MEMANTO integration — persistent memory for cross-session context (Phases 1-4)
Phase 1: Core Client
- New class-memanto-client.php: Singleton PHP client for MEMANTO API v2
- Health check with 5-min transient caching
- Agent CRUD (ensure, activate, deactivate sessions)
- Memory operations (remember, batch_remember, recall, recall_recent)
- Auto re-activation on expired session tokens (401 retry)
Phase 2: Write-Through Memory Hooks
- New class-memanto-context-enhancer.php: Orchestrates remember/recall
- Fires on: user message, plan generated/approved/rejected,
section written, block refined, config saved, session start/end
- All hooks via do_action() — zero coupling to MEMANTO when disabled
Phase 3: Context Enrichment
- Context builder injects recalled memories into AI prompts
via build_memanto_context() in build_working_context()
- 3-recall strategy: recent post memories, semantic search, user preferences
- Deduplication by content hash
Phase 4: Cross-Session Restore
- New REST endpoints: /memanto/restore, /memanto/preferences
- restore_session() recalls 15 recent memories + user preferences on editor load
- build_session_restore_message() creates AI-ready system message
- get_user_preferences_for_new_post() extracts tone/audience/length/language
- Frontend: 🧠 Restored badge in status bar with memory count tooltip
- Preference carry-over: auto-fills post config from stored user preferences
- deactivate_session() called on session end (triggers MEMANTO summary)
- Badge clears on new conversation start
Settings UI:
- MEMANTO Context Keeper section with enable toggle, URL, API key, test connection
- Settings registered via class-settings-v2.php + tab-memanto.php view
Graceful degradation: all MEMANTO calls guarded by is_active(),
frontend catches silently, plugin works identically when disabled.
This commit is contained in:
@@ -66,7 +66,9 @@
|
||||
color: #a7aaad;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
transition: color 0.1s ease, border-color 0.1s ease;
|
||||
transition:
|
||||
color 0.1s ease,
|
||||
border-color 0.1s ease;
|
||||
margin-bottom: -1px;
|
||||
}
|
||||
|
||||
@@ -176,6 +178,188 @@
|
||||
background: #525b6b;
|
||||
}
|
||||
|
||||
.wpaw-agent-workspace-card {
|
||||
background: linear-gradient(135deg, #111827 0%, #1e293b 100%);
|
||||
border: 1px solid #334155;
|
||||
border-radius: 12px;
|
||||
margin: 10px 10px 8px;
|
||||
padding: 12px;
|
||||
color: #e5e7eb;
|
||||
box-shadow: 0 12px 30px rgba(15, 23, 42, 0.28);
|
||||
}
|
||||
|
||||
.wpaw-agent-workspace-card.is-collapsed {
|
||||
padding: 9px 10px;
|
||||
}
|
||||
|
||||
.wpaw-agent-workspace-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.wpaw-agent-workspace-card.is-collapsed .wpaw-agent-workspace-header {
|
||||
align-items: center;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.wpaw-agent-workspace-heading {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.wpaw-agent-workspace-kicker {
|
||||
color: #93c5fd;
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
font-weight: 700;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
||||
.wpaw-agent-workspace-title {
|
||||
color: #f8fafc;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.wpaw-agent-workspace-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.wpaw-agent-workspace-status {
|
||||
white-space: nowrap;
|
||||
border-radius: 999px;
|
||||
border: 1px solid rgba(148, 163, 184, 0.35);
|
||||
color: #cbd5e1;
|
||||
background: rgba(15, 23, 42, 0.7);
|
||||
padding: 4px 8px;
|
||||
font-size: 10px;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.wpaw-agent-workspace-toggle {
|
||||
border: 1px solid rgba(147, 197, 253, 0.35);
|
||||
border-radius: 999px;
|
||||
background: rgba(15, 23, 42, 0.42);
|
||||
color: #bfdbfe;
|
||||
cursor: pointer;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
padding: 5px 8px;
|
||||
}
|
||||
|
||||
.wpaw-agent-workspace-toggle:hover,
|
||||
.wpaw-agent-workspace-toggle:focus {
|
||||
border-color: #60a5fa;
|
||||
color: #eff6ff;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.wpaw-agent-workspace-status.status-in_progress,
|
||||
.wpaw-agent-workspace-status.status-paused,
|
||||
.wpaw-agent-workspace-status.status-running,
|
||||
.wpaw-agent-workspace-status.status-stopping {
|
||||
color: #fbbf24;
|
||||
border-color: rgba(251, 191, 36, 0.45);
|
||||
background: rgba(113, 63, 18, 0.28);
|
||||
}
|
||||
|
||||
.wpaw-agent-workspace-status.status-completed {
|
||||
color: #86efac;
|
||||
border-color: rgba(134, 239, 172, 0.45);
|
||||
background: rgba(20, 83, 45, 0.28);
|
||||
}
|
||||
|
||||
.wpaw-agent-workspace-status.status-failed {
|
||||
color: #fca5a5;
|
||||
border-color: rgba(248, 113, 113, 0.45);
|
||||
background: rgba(127, 29, 29, 0.28);
|
||||
}
|
||||
|
||||
.wpaw-agent-context-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.wpaw-agent-context-item {
|
||||
min-width: 0;
|
||||
background: rgba(15, 23, 42, 0.52);
|
||||
border: 1px solid rgba(148, 163, 184, 0.18);
|
||||
border-radius: 9px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.wpaw-agent-context-item span {
|
||||
display: block;
|
||||
color: #94a3b8;
|
||||
font-size: 10px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.wpaw-agent-context-item strong {
|
||||
display: block;
|
||||
color: #f8fafc;
|
||||
font-size: 12px;
|
||||
line-height: 1.35;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.wpaw-agent-keyword-input {
|
||||
width: 100%;
|
||||
min-height: 26px;
|
||||
border: 1px solid rgba(147, 197, 253, 0.32);
|
||||
border-radius: 7px;
|
||||
background: rgba(15, 23, 42, 0.78);
|
||||
color: #f8fafc;
|
||||
font-size: 12px;
|
||||
padding: 4px 7px;
|
||||
}
|
||||
|
||||
.wpaw-agent-keyword-input:focus {
|
||||
outline: none;
|
||||
border-color: #60a5fa;
|
||||
box-shadow: 0 0 0 1px rgba(96, 165, 250, 0.35);
|
||||
}
|
||||
|
||||
.wpaw-agent-resume-card {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-top: 10px;
|
||||
padding: 9px;
|
||||
border-radius: 9px;
|
||||
background: rgba(59, 130, 246, 0.12);
|
||||
border: 1px solid rgba(96, 165, 250, 0.28);
|
||||
}
|
||||
|
||||
.wpaw-agent-resume-card strong,
|
||||
.wpaw-agent-resume-card span {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.wpaw-agent-resume-card strong {
|
||||
color: #bfdbfe;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.wpaw-agent-resume-card span {
|
||||
color: #93c5fd;
|
||||
font-size: 11px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.wpaw-input-area {
|
||||
background: #1e2128;
|
||||
padding: 12px;
|
||||
@@ -581,7 +765,7 @@ input.wpaw-plan-section-check:checked::before {
|
||||
}
|
||||
|
||||
.wpaw-block-refining::before {
|
||||
content: 'REFINING';
|
||||
content: "REFINING";
|
||||
position: absolute;
|
||||
top: -12px;
|
||||
right: 8px;
|
||||
@@ -618,7 +802,7 @@ input.wpaw-plan-section-check:checked::before {
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
overflow-x: auto;
|
||||
font-family: ui-monospace, 'SF Mono', Menlo, monospace;
|
||||
font-family: ui-monospace, "SF Mono", Menlo, monospace;
|
||||
font-size: 12px;
|
||||
border: 1px solid #2d3139;
|
||||
color: #c8cdd5;
|
||||
@@ -629,16 +813,18 @@ input.wpaw-plan-section-check:checked::before {
|
||||
color: #a5d6ff;
|
||||
padding: 2px 5px;
|
||||
border-radius: 4px;
|
||||
font-family: ui-monospace, 'SF Mono', Menlo, monospace;
|
||||
font-family: ui-monospace, "SF Mono", Menlo, monospace;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.wpaw-editor-locked .admin-ui-navigable-region.interface-interface-skeleton__content {
|
||||
.wpaw-editor-locked
|
||||
.admin-ui-navigable-region.interface-interface-skeleton__content {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.wpaw-editor-locked .admin-ui-navigable-region.interface-interface-skeleton__content::after {
|
||||
content: '';
|
||||
.wpaw-editor-locked
|
||||
.admin-ui-navigable-region.interface-interface-skeleton__content::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(255, 255, 255, 0.55);
|
||||
@@ -894,7 +1080,7 @@ input.wpaw-plan-section-check:checked::before {
|
||||
}
|
||||
|
||||
.wpaw-ai-response .wpaw-response::before {
|
||||
content: '';
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: -15px;
|
||||
top: 20px;
|
||||
@@ -913,7 +1099,7 @@ input.wpaw-plan-section-check:checked::before {
|
||||
}
|
||||
|
||||
.wpaw-streaming-indicator::after {
|
||||
content: '...';
|
||||
content: "...";
|
||||
display: inline-block;
|
||||
width: 18px;
|
||||
animation: wpaw-ellipsis 1.1s infinite;
|
||||
@@ -951,19 +1137,19 @@ input.wpaw-plan-section-check:checked::before {
|
||||
|
||||
@keyframes wpaw-ellipsis {
|
||||
0% {
|
||||
content: '.';
|
||||
content: ".";
|
||||
}
|
||||
|
||||
33% {
|
||||
content: '..';
|
||||
content: "..";
|
||||
}
|
||||
|
||||
66% {
|
||||
content: '...';
|
||||
content: "...";
|
||||
}
|
||||
|
||||
100% {
|
||||
content: '.';
|
||||
content: ".";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1073,7 +1259,7 @@ input.wpaw-plan-section-check:checked::before {
|
||||
border-color: #e2e8f0;
|
||||
}
|
||||
|
||||
.wpaw-plan-section-row input[type=checkbox] {
|
||||
.wpaw-plan-section-row input[type="checkbox"] {
|
||||
transform: translateY(3px);
|
||||
}
|
||||
|
||||
@@ -1093,7 +1279,8 @@ input.wpaw-plan-section-check:checked::before {
|
||||
|
||||
.wpaw-timeline-content {
|
||||
flex: 1;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
|
||||
font-family:
|
||||
-apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
|
||||
color: #c8cdd5;
|
||||
}
|
||||
|
||||
@@ -1181,7 +1368,7 @@ input.wpaw-plan-section-check:checked::before {
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.wpaw-config-tab>*:nth-child(2) {
|
||||
.wpaw-config-tab > *:nth-child(2) {
|
||||
margin-top: 60px;
|
||||
}
|
||||
|
||||
@@ -1293,14 +1480,16 @@ input.wpaw-plan-section-check:checked::before {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.wpaw-budget-bar~.description {
|
||||
.wpaw-budget-bar ~ .description {
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
.wpaw-budget-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #4caf50, #66bb6a);
|
||||
transition: width 0.5s ease, background 0.3s ease;
|
||||
transition:
|
||||
width 0.5s ease,
|
||||
background 0.3s ease;
|
||||
}
|
||||
|
||||
.wpaw-budget-fill.warning {
|
||||
@@ -1602,7 +1791,7 @@ input.wpaw-plan-section-check:checked::before {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.wpaw-previous-answers>div:last-child .wpaw-answer-text {
|
||||
.wpaw-previous-answers > div:last-child .wpaw-answer-text {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
@@ -1722,11 +1911,11 @@ input.wpaw-plan-section-check:checked::before {
|
||||
transition: 0.3s;
|
||||
}
|
||||
|
||||
.wpaw-config-toggle input:checked+.wpaw-toggle-slider {
|
||||
.wpaw-config-toggle input:checked + .wpaw-toggle-slider {
|
||||
background-color: #2271b1;
|
||||
}
|
||||
|
||||
.wpaw-config-toggle input:checked+.wpaw-toggle-slider:before {
|
||||
.wpaw-config-toggle input:checked + .wpaw-toggle-slider:before {
|
||||
transform: translateX(24px);
|
||||
}
|
||||
|
||||
@@ -1755,11 +1944,14 @@ input.wpaw-plan-section-check:checked::before {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.wpaw-question-card .wpaw-config-form .wpaw-config-label .wpaw-config-description {
|
||||
.wpaw-question-card
|
||||
.wpaw-config-form
|
||||
.wpaw-config-label
|
||||
.wpaw-config-description {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.wpaw-question-card .wpaw-config-field:has(input[type=text]) {
|
||||
.wpaw-question-card .wpaw-config-field:has(input[type="text"]) {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
@@ -1786,7 +1978,7 @@ input.wpaw-plan-section-check:checked::before {
|
||||
padding-right: 20px;
|
||||
}
|
||||
|
||||
.wpaw-question-card .wpaw-config-form .wpaw-config-field input[type=text] {
|
||||
.wpaw-question-card .wpaw-config-form .wpaw-config-field input[type="text"] {
|
||||
background-color: #1a1a1a !important;
|
||||
}
|
||||
|
||||
@@ -1812,13 +2004,12 @@ input.wpaw-plan-section-check:checked::before {
|
||||
}
|
||||
|
||||
.dark-theme .wpaw-question-card textarea::placeholder {
|
||||
color: #6c6c6c
|
||||
color: #6c6c6c;
|
||||
}
|
||||
|
||||
.dark-theme .wpaw-question-card textarea::focus,
|
||||
.dark-theme .wpaw-question-card textarea::active {
|
||||
border-color: #252830 !important;
|
||||
;
|
||||
}
|
||||
|
||||
/* ===========================
|
||||
@@ -1861,7 +2052,6 @@ input.wpaw-plan-section-check:checked::before {
|
||||
}
|
||||
|
||||
@keyframes wpaw-pulse {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
box-shadow: 0 0 0 0px rgba(34, 113, 177, 0.2);
|
||||
@@ -1920,8 +2110,9 @@ input.wpaw-plan-section-check:checked::before {
|
||||
}
|
||||
|
||||
@media (max-width: 482px) {
|
||||
|
||||
.interface-complementary-area__fill:has(#wp-agentic-writer\:wp-agentic-writer),
|
||||
.interface-complementary-area__fill:has(
|
||||
#wp-agentic-writer\:wp-agentic-writer
|
||||
),
|
||||
#wp-agentic-writer\:wp-agentic-writer {
|
||||
width: 100vw !important;
|
||||
}
|
||||
@@ -1933,7 +2124,9 @@ input.wpaw-plan-section-check:checked::before {
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background-color: #3b82f6;
|
||||
box-shadow: 12px 0 #3b82f6, -12px 0 #3b82f6;
|
||||
box-shadow:
|
||||
12px 0 #3b82f6,
|
||||
-12px 0 #3b82f6;
|
||||
position: relative;
|
||||
animation: wpaw-flash 0.5s ease-out infinite alternate;
|
||||
margin: 0 20px 0 16px;
|
||||
@@ -1943,17 +2136,23 @@ input.wpaw-plan-section-check:checked::before {
|
||||
@keyframes wpaw-flash {
|
||||
0% {
|
||||
background-color: #93c5fd;
|
||||
box-shadow: 12px 0 #93c5fd, -12px 0 #3b82f6;
|
||||
box-shadow:
|
||||
12px 0 #93c5fd,
|
||||
-12px 0 #3b82f6;
|
||||
}
|
||||
|
||||
50% {
|
||||
background-color: #3b82f6;
|
||||
box-shadow: 12px 0 #93c5fd, -12px 0 #93c5fd;
|
||||
box-shadow:
|
||||
12px 0 #93c5fd,
|
||||
-12px 0 #93c5fd;
|
||||
}
|
||||
|
||||
100% {
|
||||
background-color: #93c5fd;
|
||||
box-shadow: 12px 0 #3b82f6, -12px 0 #93c5fd;
|
||||
box-shadow:
|
||||
12px 0 #3b82f6,
|
||||
-12px 0 #93c5fd;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1977,7 +2176,8 @@ input.wpaw-plan-section-check:checked::before {
|
||||
padding: 8px 12px;
|
||||
background: #1d2227;
|
||||
color: #fff;
|
||||
font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
|
||||
font-family:
|
||||
ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
|
||||
font-size: 12px;
|
||||
border-bottom: 1px solid #3c3c3c;
|
||||
}
|
||||
@@ -2004,11 +2204,22 @@ input.wpaw-plan-section-check:checked::before {
|
||||
animation: statusPulse 1s infinite;
|
||||
}
|
||||
|
||||
.wpaw-status-dot.checking,
|
||||
.wpaw-status-dot.refining {
|
||||
background: #60a5fa;
|
||||
animation: statusPulse 0.8s infinite;
|
||||
}
|
||||
|
||||
.wpaw-status-dot.writing {
|
||||
background: #2271b1;
|
||||
animation: statusPulse 0.8s infinite;
|
||||
}
|
||||
|
||||
.wpaw-status-dot.stopping {
|
||||
background: #f97316;
|
||||
animation: statusPulse 0.55s infinite;
|
||||
}
|
||||
|
||||
.wpaw-status-dot.complete {
|
||||
background: #00a32a;
|
||||
}
|
||||
@@ -2018,7 +2229,6 @@ input.wpaw-plan-section-check:checked::before {
|
||||
}
|
||||
|
||||
@keyframes statusPulse {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
@@ -2035,6 +2245,22 @@ input.wpaw-plan-section-check:checked::before {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.wpaw-memanto-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 10px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.02em;
|
||||
background: rgba(99, 155, 255, 0.15);
|
||||
color: #93b8ff;
|
||||
border: 1px solid rgba(99, 155, 255, 0.25);
|
||||
cursor: default;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.wpaw-status-cost {
|
||||
color: #a7aaad;
|
||||
font-size: 11px;
|
||||
@@ -2047,7 +2273,8 @@ input.wpaw-plan-section-check:checked::before {
|
||||
flex-direction: column;
|
||||
overflow-y: auto;
|
||||
background: #fff;
|
||||
font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
|
||||
font-family:
|
||||
ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
@@ -2127,7 +2354,9 @@ input.wpaw-plan-section-check:checked::before {
|
||||
/* Agent Response (prose) */
|
||||
.wpaw-log-entry.agent-response {
|
||||
border-left-color: #dcdcde;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
|
||||
font-family:
|
||||
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans,
|
||||
Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
|
||||
}
|
||||
|
||||
/* Command Input Area */
|
||||
@@ -2287,7 +2516,9 @@ input.wpaw-plan-section-check:checked::before {
|
||||
font-size: 14px;
|
||||
padding: 2px;
|
||||
line-height: 1;
|
||||
transition: color 0.1s ease, transform 0.1s ease;
|
||||
transition:
|
||||
color 0.1s ease,
|
||||
transform 0.1s ease;
|
||||
}
|
||||
|
||||
.wpaw-status-icon-btn:hover {
|
||||
@@ -2391,11 +2622,11 @@ input.wpaw-plan-section-check:checked::before {
|
||||
transition: opacity 0.15s ease;
|
||||
}
|
||||
|
||||
.wpaw-web-search-toggle input:checked+.wpaw-web-search-icon {
|
||||
.wpaw-web-search-toggle input:checked + .wpaw-web-search-icon {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.wpaw-web-search-toggle input:checked+.wpaw-web-search-icon * {
|
||||
.wpaw-web-search-toggle input:checked + .wpaw-web-search-icon * {
|
||||
stroke: #4caf50;
|
||||
}
|
||||
|
||||
@@ -2408,7 +2639,7 @@ input.wpaw-plan-section-check:checked::before {
|
||||
transition: color 0.15s ease;
|
||||
}
|
||||
|
||||
.wpaw-web-search-toggle input:checked~.wpaw-web-search-label {
|
||||
.wpaw-web-search-toggle input:checked ~ .wpaw-web-search-label {
|
||||
color: #4caf50;
|
||||
}
|
||||
|
||||
@@ -2513,6 +2744,17 @@ input.wpaw-plan-section-check:checked::before {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.wpaw-stop-circle-btn.is-stopping,
|
||||
.wpaw-stop-circle-btn.is-stopping:hover {
|
||||
background: #f97316;
|
||||
cursor: wait;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.wpaw-stop-spinner {
|
||||
animation: wpaw-spin 0.85s linear infinite;
|
||||
}
|
||||
|
||||
.wpaw-command-circle-btn svg {
|
||||
width: 20px !important;
|
||||
height: 20px !important;
|
||||
@@ -2827,10 +3069,37 @@ input.wpaw-plan-section-check:checked::before {
|
||||
}
|
||||
|
||||
.wpaw-seo-check .check-label {
|
||||
flex: 1;
|
||||
color: #a7aaad;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.wpaw-meta-info>button.components-button.is-secondary.is-small {
|
||||
.wpaw-seo-fix-button.components-button.is-secondary.is-small {
|
||||
border-color: rgba(96, 165, 250, 0.72);
|
||||
box-shadow: none !important;
|
||||
color: #bfdbfe;
|
||||
flex-shrink: 0;
|
||||
height: 24px;
|
||||
min-width: 44px;
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.wpaw-seo-fix-button.components-button.is-secondary.is-small:hover:not(
|
||||
:disabled
|
||||
),
|
||||
.wpaw-seo-fix-button.components-button.is-secondary.is-small:focus:not(
|
||||
:disabled
|
||||
) {
|
||||
border-color: #60a5fa;
|
||||
color: #eff6ff;
|
||||
}
|
||||
|
||||
.wpaw-seo-fix-button.components-button.is-secondary.is-small.is-fixing {
|
||||
border-color: #fbbf24;
|
||||
color: #fde68a;
|
||||
}
|
||||
|
||||
.wpaw-meta-info > button.components-button.is-secondary.is-small {
|
||||
outline: unset !important;
|
||||
color: #fbbf24;
|
||||
border: 1px solid #fbbf24;
|
||||
@@ -3368,7 +3637,9 @@ input.wpaw-plan-section-check:checked::before {
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.2s, background 0.2s;
|
||||
transition:
|
||||
border-color 0.2s,
|
||||
background 0.2s;
|
||||
}
|
||||
|
||||
.wpaw-fk-select {
|
||||
@@ -3386,7 +3657,9 @@ input.wpaw-plan-section-check:checked::before {
|
||||
padding: 6px 10px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
transition: border-color 0.2s, background 0.2s;
|
||||
transition:
|
||||
border-color 0.2s,
|
||||
background 0.2s;
|
||||
}
|
||||
|
||||
.wpaw-fk-input:focus {
|
||||
@@ -3411,7 +3684,9 @@ input.wpaw-plan-section-check:checked::before {
|
||||
padding: 10px 12px;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
transition: border-color 0.2s, background 0.2s;
|
||||
transition:
|
||||
border-color 0.2s,
|
||||
background 0.2s;
|
||||
}
|
||||
|
||||
.wpaw-fk-custom-input:focus {
|
||||
@@ -3460,7 +3735,9 @@ input.wpaw-plan-section-check:checked::before {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: background 0.2s, color 0.2s;
|
||||
transition:
|
||||
background 0.2s,
|
||||
color 0.2s;
|
||||
}
|
||||
|
||||
.wpaw-fk-expand:hover,
|
||||
@@ -3618,7 +3895,9 @@ input.wpaw-plan-section-check:checked::before {
|
||||
font-size: 14px;
|
||||
margin-bottom: 1rem;
|
||||
box-sizing: border-box;
|
||||
transition: border-color 0.2s, background 0.2s;
|
||||
transition:
|
||||
border-color 0.2s,
|
||||
background 0.2s;
|
||||
}
|
||||
|
||||
.wpaw-welcome-input:focus {
|
||||
@@ -3919,7 +4198,6 @@ input.wpaw-plan-section-check:checked::before {
|
||||
P2: TYPING ANIMATION
|
||||
=========================== */
|
||||
@keyframes wpaw-typewriter-cursor {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
border-color: transparent;
|
||||
@@ -3962,7 +4240,6 @@ input.wpaw-plan-section-check:checked::before {
|
||||
}
|
||||
|
||||
@keyframes wpaw-typing-bounce {
|
||||
|
||||
0%,
|
||||
60%,
|
||||
100% {
|
||||
@@ -4030,8 +4307,13 @@ input.wpaw-plan-section-check:checked::before {
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.5; }
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
.wpaw-suggestion-item {
|
||||
@@ -4169,7 +4451,9 @@ input.wpaw-plan-section-check:checked::before {
|
||||
max-width: 90vw;
|
||||
background: #2d2d2d;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(255, 255, 255, 0.1);
|
||||
box-shadow:
|
||||
0 20px 60px rgba(0, 0, 0, 0.5),
|
||||
0 0 0 1px rgba(255, 255, 255, 0.1);
|
||||
overflow: hidden;
|
||||
animation: wpaw-palette-slide-in 0.15s ease-out;
|
||||
}
|
||||
@@ -4550,7 +4834,6 @@ input.wpaw-plan-section-check:checked::before {
|
||||
color: #f59e0b;
|
||||
}
|
||||
|
||||
|
||||
/* ===========================
|
||||
AUDIT FIXES: Mode Indicator Badge
|
||||
=========================== */
|
||||
@@ -4567,10 +4850,20 @@ input.wpaw-plan-section-check:checked::before {
|
||||
margin-bottom: 0.5em !important;
|
||||
letter-spacing: normal !important;
|
||||
}
|
||||
.wpaw-response-content h1 {
|
||||
font-size: 20px !important;
|
||||
color: #e8ecf2 !important;
|
||||
font-weight: bold;
|
||||
margin-top: 1.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.wpaw-response-content h2 {
|
||||
font-size: 17px !important;
|
||||
color: #e8ecf2 !important;
|
||||
font-weight: bold;
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.wpaw-response-content h4,
|
||||
@@ -4580,6 +4873,17 @@ input.wpaw-plan-section-check:checked::before {
|
||||
color: #d0d5dd !important;
|
||||
}
|
||||
|
||||
.wpaw-response-content table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.wpaw-response-content table th,
|
||||
table td {
|
||||
border: 1px solid #dce0e8 !important;
|
||||
padding: 4px 6px;
|
||||
}
|
||||
|
||||
.wpaw-mode-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -4763,7 +5067,7 @@ input.wpaw-plan-section-check:checked::before {
|
||||
color: #dce0e8;
|
||||
}
|
||||
|
||||
.wpaw-response-content>* {
|
||||
.wpaw-response-content > * {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
@@ -4801,7 +5105,8 @@ input.wpaw-plan-section-check:checked::before {
|
||||
|
||||
.wpaw-config-summary-item {
|
||||
color: #9aa5b4;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
|
||||
font-family:
|
||||
-apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
|
||||
margin-bottom: 4px;
|
||||
font-size: 11.5px;
|
||||
}
|
||||
|
||||
19396
assets/js/sidebar.js
19396
assets/js/sidebar.js
File diff suppressed because it is too large
Load Diff
435
docs/implementation/MEMANTO_INTEGRATION_PLAN.md
Normal file
435
docs/implementation/MEMANTO_INTEGRATION_PLAN.md
Normal file
@@ -0,0 +1,435 @@
|
||||
# MEMANTO Integration Plan — Optional Context Enhancement
|
||||
|
||||
**Version:** 1.0
|
||||
**Date:** 2026-06-07
|
||||
**Status:** Planning
|
||||
**Depends on:** MEMANTO_PRICING_STRATEGY.md
|
||||
|
||||
---
|
||||
|
||||
## Design Principles
|
||||
|
||||
1. **MEMANTO is optional.** The plugin's built-in Context Builder (`class-context-builder.php`) remains the default and always works without MEMANTO.
|
||||
2. **MEMANTO enhances, never replaces.** MySQL sessions (`wpaw_conversations`) remain the primary session store. MEMANTO runs parallel.
|
||||
3. **Zero disruption on failure.** If MEMANTO is unreachable, the plugin falls back to existing behavior with no error shown to the user.
|
||||
4. **Server-side only.** All MEMANTO API calls happen in PHP. The frontend (sidebar.js) is unaware of MEMANTO — it just sees richer or leaner context in AI responses.
|
||||
5. **User brings own Moorcheh key.** Plugin stores MEMANTO URL + Moorcheh API key in WordPress settings. Never hardcoded.
|
||||
|
||||
---
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
```
|
||||
┌────────────────────────────────────────────────┐
|
||||
│ WordPress Backend │
|
||||
│ │
|
||||
User Message ──────► │ Gutenberg Sidebar (handle_chat_request) │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ Context Builder (build_system_message) │
|
||||
│ │ │
|
||||
│ ├──► MySQL Session (wpaw_conversations) │
|
||||
│ │ primary store │
|
||||
│ │ │
|
||||
│ ├──► Memanto Client ──► MEMANTO API │
|
||||
│ │ (if configured) │
|
||||
│ │ │ │
|
||||
│ │ ├── recall (retrieve) │
|
||||
│ │ └── remember (store) │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ Merged Context → AI Provider → Response │
|
||||
└────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Data Flow: Two Paths
|
||||
|
||||
| Path | When | What Happens |
|
||||
|---|---|---|
|
||||
| **Default (no MEMANTO)** | MEMANTO URL not configured in settings | Context Builder uses MySQL session only. Identical to current behavior. |
|
||||
| **MEMANTO active** | MEMANTO URL + Moorcheh key configured and validated | Context Builder queries MEMANTO for relevant memories before building context. After AI response, significant events are stored in MEMANTO. |
|
||||
|
||||
---
|
||||
|
||||
## Agent Design
|
||||
|
||||
### Agent Naming Convention
|
||||
|
||||
| Agent ID | Scope | Purpose |
|
||||
|---|---|---|
|
||||
| `wp-user-{wordpress_user_id}` | Per WordPress user | Cross-post preferences: writing style, tone, audience, language, brand voice |
|
||||
| `wp-post-{wordpress_post_id}` | Per post | Article-specific: plan decisions, rejections, research, section progress |
|
||||
|
||||
### Memory Types Used
|
||||
|
||||
| MEMANTO Type | When Stored | Example |
|
||||
|---|---|---|
|
||||
| `preference` | User sets/changes post config | "User prefers conversational tone, intermediate audience" |
|
||||
| `instruction` | User sends chat message | "Focus on plugin vulnerabilities only" |
|
||||
| `decision` | User approves/rejects plan | "Approved 5-section outline for WordPress security" |
|
||||
| `artifact` | Plan generated, section written | "Plan: 5 sections covering X, Y, Z" |
|
||||
| `context` | Session ends / summarize | "Article at 60% completion, 3 of 5 sections done" |
|
||||
| `error` | User corrects AI output | "User rejected generic tips approach, wants specific plugin recommendations" |
|
||||
|
||||
### Tags Convention
|
||||
|
||||
Tags enable targeted recall. Every memory includes:
|
||||
|
||||
| Tag | Example | Purpose |
|
||||
|---|---|---|
|
||||
| `post:{id}` | `post:42` | Scope recall to specific post |
|
||||
| `site:{domain}` | `site:example.com` | Scope to WordPress site |
|
||||
| `mode:{mode}` | `mode:planning` | What mode was active |
|
||||
| `model:{model}` | `model:deepseek-chat` | Which model was used |
|
||||
|
||||
---
|
||||
|
||||
## New Files to Create
|
||||
|
||||
### `includes/class-memanto-client.php`
|
||||
|
||||
PHP client for MEMANTO API v2. Singleton class.
|
||||
|
||||
**Public Methods:**
|
||||
|
||||
| Method | Description |
|
||||
|---|---|
|
||||
| `is_configured()` | Returns true if MEMANTO URL + Moorcheh key are set in settings |
|
||||
| `is_healthy()` | Calls `/health` endpoint, caches result for 5 minutes |
|
||||
| `ensure_agent( $agent_id )` | Creates agent via `POST /api/v2/agents` if not exists |
|
||||
| `activate_session( $agent_id )` | `POST /api/v2/agents/{id}/activate`, caches session token in transient |
|
||||
| `remember( $agent_id, $content, $type, $tags, $title )` | `POST /api/v2/agents/{id}/remember` |
|
||||
| `batch_remember( $agent_id, $memories )` | `POST /api/v2/agents/{id}/batch-remember` |
|
||||
| `recall( $agent_id, $query, $type, $limit )` | `POST /api/v2/agents/{id}/recall` |
|
||||
| `recall_recent( $agent_id, $limit )` | `POST /api/v2/agents/{id}/recall/recent` |
|
||||
| `deactivate_session( $agent_id )` | `POST /api/v2/agents/{id}/deactivate` |
|
||||
|
||||
**Internal Mechanics:**
|
||||
|
||||
- Session token stored in WP transient: `wpaw_memanto_token_{agent_id}` (6-hour TTL matching MEMANTO JWT)
|
||||
- Auto-reactivates on expired token (catches 401, re-activates, retries)
|
||||
- All calls use `wp_remote_post` / `wp_remote_get` with 10-second timeout
|
||||
- All calls wrapped in try/catch with `wpaw_debug_log` on failure
|
||||
- Moorcheh API key passed via `X-API-Key` header or configured in MEMANTO instance (depending on MEMANTO's auth model)
|
||||
|
||||
### `includes/class-memanto-context-enhancer.php`
|
||||
|
||||
Orchestrates when and what to remember/recall. Hooks into existing Context Service.
|
||||
|
||||
**Public Methods:**
|
||||
|
||||
| Method | Hook Point | Description |
|
||||
|---|---|---|
|
||||
| `on_session_start( $session_id, $post_id, $user_id )` | Session creation | Ensures user + post agents exist; recalls previous session state |
|
||||
| `on_user_message( $session_id, $content, $post_id )` | After user sends message | Stores instruction-type memory |
|
||||
| `on_plan_generated( $post_id, $plan )` | After plan creation | Stores artifact-type memory |
|
||||
| `on_plan_approved( $post_id, $plan )` | User approves plan | Stores decision-type memory |
|
||||
| `on_plan_rejected( $post_id, $reason )` | User rejects/requests changes | Stores error-type memory with rejection reason |
|
||||
| `on_section_written( $post_id, $section_id, $summary )` | After section generation | Stores artifact-type memory |
|
||||
| `on_block_refined( $post_id, $block_id, $instruction )` | After refinement | Stores instruction-type memory |
|
||||
| `on_config_saved( $post_id, $config )` | Post config updated | Stores preference-type memory to both user and post agents |
|
||||
| `on_session_end( $session_id, $post_id )` | Session completed/archived | Summarizes session, stores context-type memory, deactivates session |
|
||||
| `recall_for_context( $post_id, $user_id, $current_message )` | Before building context | Returns recalled memories to enrich prompt |
|
||||
|
||||
**Recall Strategy (`recall_for_context`):**
|
||||
|
||||
1. Recall recent memories from post agent (limit: 10)
|
||||
2. Recall semantically relevant memories from post agent (query: user's current message, limit: 5)
|
||||
3. Recall user preferences from user agent (query: "writing preferences tone audience", limit: 5)
|
||||
4. Deduplicate by content hash
|
||||
5. Return structured array of recalled items
|
||||
|
||||
---
|
||||
|
||||
## Files to Modify
|
||||
|
||||
### `includes/class-settings-v2.php`
|
||||
|
||||
Add MEMANTO configuration section:
|
||||
|
||||
```
|
||||
MEMANTO Context Keeper
|
||||
├── Enable MEMANTO integration (checkbox, default: off)
|
||||
├── MEMANTO Instance URL (text, e.g., https://abc123.context.wpagentic.dev)
|
||||
├── Moorcheh API Key (password, user's own key)
|
||||
└── Connection Status (read-only, shows "Connected" / "Not configured" / "Error: ...")
|
||||
```
|
||||
|
||||
Add a "Test Connection" button that calls MEMANTO `/health` endpoint.
|
||||
|
||||
### `includes/class-context-builder.php`
|
||||
|
||||
Modify `build_for_task()` method. After line ~52 where `$saved_context` is loaded:
|
||||
|
||||
```php
|
||||
// Existing: MySQL context
|
||||
$saved_context = $context_service->get_context( $session_id, $post_id );
|
||||
|
||||
// NEW: MEMANTO enhancement (if configured)
|
||||
$memanto_context = array();
|
||||
$memanto_client = WP_Agentic_Writer_Memanto_Client::get_instance();
|
||||
if ( $memanto_client->is_configured() && $memanto_client->is_healthy() ) {
|
||||
$enhancer = WP_Agentic_Writer_Memanto_Context_Enhancer::get_instance();
|
||||
$memanto_context = $enhancer->recall_for_context(
|
||||
$post_id,
|
||||
get_current_user_id(),
|
||||
$request_params['latestUserMessage'] ?? ''
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
Modify `build_working_context()` to include a new section:
|
||||
|
||||
```php
|
||||
// After existing sections, before "Recent saved conversation excerpts"
|
||||
if ( ! empty( $memanto_context ) ) {
|
||||
$memory_lines = $this->format_memanto_memories( $memanto_context );
|
||||
if ( '' !== $memory_lines ) {
|
||||
$sections[] = "PERSISTENT MEMORY (recalled from MEMANTO):\n" . $memory_lines;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Key rule:** MEMANTO context is **additive**. It never replaces the existing `BACKEND CONTINUITY CONTEXT` section. It supplements it.
|
||||
|
||||
### `includes/class-context-service.php`
|
||||
|
||||
Add MEMANTO write-through hooks in key methods:
|
||||
|
||||
| Method | Hook Added |
|
||||
|---|---|
|
||||
| `save_plan()` | `$enhancer->on_plan_generated( $post_id, $plan )` |
|
||||
| `update_session_context()` | `$enhancer->on_config_saved()` if config changed |
|
||||
| `add_message()` | `$enhancer->on_user_message()` for user-role messages |
|
||||
| `clear_context()` | Optionally clear MEMANTO post agent memories |
|
||||
|
||||
### `includes/class-gutenberg-sidebar.php`
|
||||
|
||||
Add MEMANTO hooks in key handler methods:
|
||||
|
||||
| Handler | Hook Added |
|
||||
|---|---|
|
||||
| `handle_chat_request()` | `$enhancer->on_user_message()` after saving to MySQL |
|
||||
| `handle_generate_plan()` | `$enhancer->on_plan_generated()` after successful plan |
|
||||
| `handle_execute_article()` | `$enhancer->on_section_written()` per section |
|
||||
| `handle_refine_block()` | `$enhancer->on_block_refined()` |
|
||||
| `handle_summarize_context()` | Skip AI call if MEMANTO active — return cached recall instead |
|
||||
| `handle_detect_intent()` | Skip AI call if MEMANTO active — use regex + MEMANTO context instead |
|
||||
|
||||
Add new REST endpoint:
|
||||
|
||||
```
|
||||
POST /wp-agentic-writer/v1/memanto/status
|
||||
→ Returns: { connected: bool, agent_count: int, memory_count: int, last_recall: string }
|
||||
```
|
||||
|
||||
### `includes/class-autoloader.php`
|
||||
|
||||
Register the two new classes:
|
||||
- `class-memanto-client.php` → `WP_Agentic_Writer_Memanto_Client`
|
||||
- `class-memanto-context-enhancer.php` → `WP_Agentic_Writer_Memanto_Context_Enhancer`
|
||||
|
||||
### `assets/js/sidebar.js` (minimal change)
|
||||
|
||||
No MEMANTO-specific logic needed. Optional enhancement:
|
||||
- Show a small "🧠 Memory active" indicator when MEMANTO is connected
|
||||
- Show "memories recalled: N" in the context audit display
|
||||
|
||||
---
|
||||
|
||||
## Graceful Degradation Strategy
|
||||
|
||||
```
|
||||
MEMANTO call succeeds?
|
||||
├── YES → Merge MEMANTO context into working context
|
||||
└── NO
|
||||
├── MEMANTO not configured → Use MySQL-only context (default behavior)
|
||||
├── MEMANTO timeout (>10s) → Log warning, use MySQL-only context
|
||||
├── MEMANTO 401 (token expired) → Re-activate session, retry once, then fallback
|
||||
└── MEMANTO 5xx (server error) → Log error, use MySQL-only context
|
||||
```
|
||||
|
||||
User **never** sees an error from MEMANTO. The worst case is they get the same experience as users without MEMANTO.
|
||||
|
||||
---
|
||||
|
||||
## Implementation Phases
|
||||
|
||||
### Phase 1: Core Client (Week 1)
|
||||
|
||||
**Goal:** MEMANTO client class + settings UI + connection validation
|
||||
|
||||
| Task | File | Details |
|
||||
|---|---|---|
|
||||
| Create Memanto Client | `class-memanto-client.php` | All API methods, session token management, error handling |
|
||||
| Create Context Enhancer shell | `class-memanto-context-enhancer.php` | Skeleton with `is_configured()` check on every method |
|
||||
| Add settings section | `class-settings-v2.php` | URL field, API key field, enable checkbox, test button |
|
||||
| Register in autoloader | `class-autoloader.php` | Add both new classes |
|
||||
| Add REST status endpoint | `class-gutenberg-sidebar.php` | `/memanto/status` endpoint |
|
||||
|
||||
**Validation:** Admin can configure MEMANTO URL + Moorcheh key, test connection, see "Connected" status. No functional changes to AI features yet.
|
||||
|
||||
### Phase 2: Write-Through Memory (Week 2)
|
||||
|
||||
**Goal:** Store memories on every meaningful action
|
||||
|
||||
| Task | Hook Point | Memory Type |
|
||||
|---|---|---|
|
||||
| Store on user message | `handle_chat_request()` | `instruction` |
|
||||
| Store on plan generated | `handle_generate_plan()` | `artifact` |
|
||||
| Store on plan approved | After plan save in frontend | `decision` |
|
||||
| Store on plan rejected | Plan revision flow | `error` |
|
||||
| Store on section written | `handle_execute_article()` | `artifact` |
|
||||
| Store on block refined | `handle_refine_block()` | `instruction` |
|
||||
| Store on config saved | `update_session_context()` | `preference` |
|
||||
| Store on session end | Session completed/archived | `context` |
|
||||
|
||||
**Validation:** Write an article with MEMANTO enabled. Check MEMANTO API (via recall endpoint) that memories were stored. Verify plugin still works perfectly with MEMANTO disabled.
|
||||
|
||||
### Phase 3: Context Enrichment (Week 3)
|
||||
|
||||
**Goal:** Recall memories to enrich AI prompts
|
||||
|
||||
| Task | File | Details |
|
||||
|---|---|---|
|
||||
| Add `recall_for_context()` | `class-memanto-context-enhancer.php` | 3-recall strategy (recent, semantic, preferences) |
|
||||
| Modify `build_for_task()` | `class-context-builder.php` | Merge recalled memories into working context |
|
||||
| Add `format_memanto_memories()` | `class-context-builder.php` | Format recalled items as compact prompt text |
|
||||
| Skip summarize-context when MEMANTO active | `class-gutenberg-sidebar.php` | Return cached recall instead of AI call |
|
||||
| Skip detect-intent when MEMANTO active | `class-gutenberg-sidebar.php` | Use regex + MEMANTO context instead |
|
||||
|
||||
**Validation:** Write an article. Mid-session, close the browser. Reopen the post. Verify AI "remembers" context from recalled memories. Compare AI response quality with/without MEMANTO.
|
||||
|
||||
### Phase 4: Cross-Session Restore (Week 4)
|
||||
|
||||
**Goal:** Seamless experience when returning to a post after days/weeks
|
||||
|
||||
| Task | Details |
|
||||
|---|---|
|
||||
| Session restore on load | When post editor opens, recall recent post memories. Build a "restored session" system message. |
|
||||
| Frontend indicator | Show "🧠 Restored from memory" badge in sidebar |
|
||||
| User preference carry-over | On new post creation, recall user agent preferences for default post config |
|
||||
| Session deactivation | On session end, call MEMANTO deactivate to trigger summary generation |
|
||||
|
||||
**Validation:** Create an article. Complete 50%. Wait 1 day. Open the post again. Verify AI picks up where it left off without user re-explaining context.
|
||||
|
||||
### Phase 5: Polish & Edge Cases (Week 5)
|
||||
|
||||
| Task | Details |
|
||||
|---|---|
|
||||
| Memory pruning | On session end, summarize verbose raw messages into compact context memories |
|
||||
| Connection health UI | Real-time status indicator in plugin sidebar header |
|
||||
| Moorcheh limit warning | When approaching 10K vectors, show admin notice with upgrade link |
|
||||
| Error logging | Detailed MEMANTO error logging with `wpaw_debug_log` |
|
||||
| Settings validation | Validate URL format, API key format, connection test before saving |
|
||||
|
||||
---
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
### Unit Tests
|
||||
|
||||
| Test | Description |
|
||||
|---|---|
|
||||
| `test_memanto_client_not_configured` | Client returns false when settings empty |
|
||||
| `test_memanto_client_health_check` | Mock `/health` response, verify caching |
|
||||
| `test_memanto_client_remember` | Mock remember API, verify payload structure |
|
||||
| `test_memanto_client_recall` | Mock recall API, verify response parsing |
|
||||
| `test_memanto_client_session_lifecycle` | Activate → remember → recall → deactivate |
|
||||
| `test_enhancer_graceful_fallback` | MEMANTO returns error, context builder still works |
|
||||
| `test_context_builder_with_memanto` | Verify MEMANTO context is included in working context |
|
||||
| `test_context_builder_without_memanto` | Verify no MEMANTO content when not configured |
|
||||
|
||||
### Integration Tests
|
||||
|
||||
| Test | Description |
|
||||
|---|---|
|
||||
| Full article with MEMANTO | Chat → Plan → Write → Refine. Verify memories stored at each step. |
|
||||
| Full article without MEMANTO | Same flow. Verify no MEMANTO calls made. Plugin works identically. |
|
||||
| MEMANTO goes down mid-session | Start with MEMANTO active. Simulate timeout. Verify graceful fallback. |
|
||||
| Cross-session restore | Write 50% of article. Simulate new session. Verify AI context restored. |
|
||||
| Multi-site with same MEMANTO | Use same MEMANTO instance across 2 sites. Verify agent isolation. |
|
||||
|
||||
### Manual Test Checklist
|
||||
|
||||
- [ ] Plugin activates with no MEMANTO settings — works normally
|
||||
- [ ] MEMANTO URL set but Moorcheh key empty — shows "not configured"
|
||||
- [ ] MEMANTO URL + invalid key — shows "connection error"
|
||||
- [ ] MEMANTO URL + valid key — shows "connected"
|
||||
- [ ] Write article with MEMANTO on — AI responses include recalled memory
|
||||
- [ ] Write article with MEMANTO off — identical to current behavior
|
||||
- [ ] Disable MEMANTO mid-session — no errors, fallback to MySQL-only
|
||||
- [ ] Re-enable MEMANTO — picks up from where it left off
|
||||
- [ ] Check MEMANTO recall endpoint — memories exist for test post
|
||||
|
||||
---
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
| Concern | Mitigation |
|
||||
|---|---|
|
||||
| MEMANTO recall adds latency to every AI call | Cache recall results in transient (5-min TTL). Only recall when context builder runs. |
|
||||
| Session token expires mid-request | Auto-reactivate on 401. Single retry. |
|
||||
| Too many memories stored | Batch-remember to reduce HTTP calls. Summarize on session end. |
|
||||
| MEMANTO instance overloaded | 10-second timeout on all calls. Graceful fallback. |
|
||||
| WordPress transient cache bloat | Use specific key patterns. Clean up on session end. |
|
||||
|
||||
---
|
||||
|
||||
## Settings UI Specification
|
||||
|
||||
### MEMANTO Context Keeper Section
|
||||
|
||||
Located in WP Agentic Writer → Settings → MEMANTO tab.
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ MEMANTO Context Keeper │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ☑ Enable MEMANTO integration │
|
||||
│ │
|
||||
│ MEMANTO Instance URL │
|
||||
│ ┌──────────────────────────────────────────────────┐ │
|
||||
│ │ https://abc123.context.wpagentic.dev │ │
|
||||
│ └──────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ Moorcheh API Key │
|
||||
│ ┌──────────────────────────────────────────────────┐ │
|
||||
│ │ •••••••••••••••••••••••• │ │
|
||||
│ └──────────────────────────────────────────────────┘ │
|
||||
│ ℹ️ Get a free API key at moorcheh.ai (10K vectors/month) │
|
||||
│ │
|
||||
│ Connection Status: 🟢 Connected │
|
||||
│ Last checked: 2 minutes ago │
|
||||
│ │
|
||||
│ [Test Connection] │
|
||||
│ │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ ℹ️ MEMANTO is an optional add-on that provides persistent │
|
||||
│ memory for your AI writing assistant. Your AI will remember │
|
||||
│ context across sessions and posts. The plugin works │
|
||||
│ perfectly without MEMANTO. │
|
||||
│ │
|
||||
│ Get MEMANTO at: wpagentic.dev/memanto │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
| Aspect | Decision |
|
||||
|---|---|
|
||||
| **Scope** | Optional enhancement, not a dependency |
|
||||
| **New files** | `class-memanto-client.php`, `class-memanto-context-enhancer.php` |
|
||||
| **Modified files** | `class-context-builder.php`, `class-context-service.php`, `class-gutenberg-sidebar.php`, `class-settings-v2.php`, `class-autoloader.php` |
|
||||
| **Frontend changes** | Minimal: status indicator only |
|
||||
| **Fallback behavior** | Full graceful degradation to MySQL-only context |
|
||||
| **Implementation time** | 5 weeks (1 week per phase) |
|
||||
| **Testing priority** | Phase 2 (write-through) and Phase 3 (recall) are critical paths |
|
||||
|
||||
---
|
||||
|
||||
**Document Date:** June 7, 2026
|
||||
**Status:** Draft — Ready for Phase 1 implementation
|
||||
File diff suppressed because it is too large
Load Diff
@@ -202,7 +202,7 @@ class WP_Agentic_Writer_Conversation_Manager {
|
||||
|
||||
$session = $wpdb->get_row(
|
||||
$wpdb->prepare(
|
||||
"SELECT * FROM {$this->table_name} WHERE post_id = %d AND status = 'active' ORDER BY updated_at DESC LIMIT 1",
|
||||
"SELECT * FROM {$this->table_name} WHERE post_id = %d AND status != 'archived' ORDER BY updated_at DESC LIMIT 1",
|
||||
$post_id
|
||||
),
|
||||
ARRAY_A
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
636
includes/class-memanto-client.php
Normal file
636
includes/class-memanto-client.php
Normal file
@@ -0,0 +1,636 @@
|
||||
<?php
|
||||
/**
|
||||
* MEMANTO Client
|
||||
*
|
||||
* PHP client for MEMANTO API v2 — external semantic memory service.
|
||||
* Provides persistent, cross-session memory for the WP Agentic Writer plugin.
|
||||
*
|
||||
* @package WP_Agentic_Writer
|
||||
* @since 0.3.0
|
||||
*/
|
||||
|
||||
if (!defined("ABSPATH")) {
|
||||
exit();
|
||||
}
|
||||
|
||||
/**
|
||||
* Class WP_Agentic_Writer_Memanto_Client
|
||||
*
|
||||
* Communicates with a MEMANTO instance (powered by Moorcheh SDK).
|
||||
* All calls are wrapped in error handling — failures never disrupt the plugin.
|
||||
*/
|
||||
class WP_Agentic_Writer_Memanto_Client
|
||||
{
|
||||
/**
|
||||
* Singleton instance.
|
||||
*
|
||||
* @var WP_Agentic_Writer_Memanto_Client
|
||||
*/
|
||||
private static $instance = null;
|
||||
|
||||
/**
|
||||
* MEMANTO base URL (e.g. https://abc123.context.wpagentic.dev).
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $base_url = "";
|
||||
|
||||
/**
|
||||
* Cached health status.
|
||||
*
|
||||
* @var array|null { healthy: bool, checked_at: int } or null if not checked yet.
|
||||
*/
|
||||
private $health_cache = null;
|
||||
|
||||
/**
|
||||
* Get singleton instance.
|
||||
*
|
||||
* @return WP_Agentic_Writer_Memanto_Client
|
||||
*/
|
||||
public static function get_instance()
|
||||
{
|
||||
if (null === self::$instance) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
private function __construct()
|
||||
{
|
||||
$settings = get_option("wp_agentic_writer_settings", []);
|
||||
$this->base_url = untrailingslashit($settings["memanto_url"] ?? "");
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Configuration & Health
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Whether MEMANTO is configured (URL + Moorcheh key set).
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function is_configured()
|
||||
{
|
||||
return !empty($this->base_url) && !empty($this->get_moorcheh_key());
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether MEMANTO is enabled in settings.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function is_enabled()
|
||||
{
|
||||
$settings = get_option("wp_agentic_writer_settings", []);
|
||||
return !empty($settings["memanto_enabled"]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether MEMANTO is enabled, configured, and reachable.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function is_active()
|
||||
{
|
||||
return $this->is_enabled() &&
|
||||
$this->is_configured() &&
|
||||
$this->is_healthy();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check MEMANTO health endpoint. Result cached for 5 minutes.
|
||||
*
|
||||
* @return bool True if healthy.
|
||||
*/
|
||||
public function is_healthy()
|
||||
{
|
||||
if (!$this->is_configured()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Use cached result if fresh (5 minutes).
|
||||
if (null !== $this->health_cache) {
|
||||
if (time() - $this->health_cache["checked_at"] < 300) {
|
||||
return $this->health_cache["healthy"];
|
||||
}
|
||||
}
|
||||
|
||||
// Also check transient for cross-request caching.
|
||||
$cached = get_transient("wpaw_memanto_health");
|
||||
if (
|
||||
false !== $cached &&
|
||||
isset($cached["checked_at"]) &&
|
||||
time() - $cached["checked_at"] < 300
|
||||
) {
|
||||
$this->health_cache = $cached;
|
||||
return $cached["healthy"];
|
||||
}
|
||||
|
||||
$response = $this->get("/health");
|
||||
|
||||
if (is_wp_error($response)) {
|
||||
$this->health_cache = ["healthy" => false, "checked_at" => time()];
|
||||
set_transient("wpaw_memanto_health", $this->health_cache, 300);
|
||||
return false;
|
||||
}
|
||||
|
||||
$healthy =
|
||||
!empty($response["status"]) && "healthy" === $response["status"];
|
||||
$this->health_cache = ["healthy" => $healthy, "checked_at" => time()];
|
||||
set_transient("wpaw_memanto_health", $this->health_cache, 300);
|
||||
return $healthy;
|
||||
}
|
||||
|
||||
/**
|
||||
* Force-refresh the health check (used by Test Connection button).
|
||||
*
|
||||
* @return array { healthy: bool, details: array|null }
|
||||
*/
|
||||
public function check_health_fresh()
|
||||
{
|
||||
if (!$this->is_configured()) {
|
||||
return ["healthy" => false, "details" => null];
|
||||
}
|
||||
|
||||
delete_transient("wpaw_memanto_health");
|
||||
$this->health_cache = null;
|
||||
|
||||
$response = $this->get("/health");
|
||||
|
||||
if (is_wp_error($response)) {
|
||||
return [
|
||||
"healthy" => false,
|
||||
"details" => ["error" => $response->get_error_message()],
|
||||
];
|
||||
}
|
||||
|
||||
$healthy =
|
||||
!empty($response["status"]) && "healthy" === $response["status"];
|
||||
$this->health_cache = ["healthy" => $healthy, "checked_at" => time()];
|
||||
set_transient("wpaw_memanto_health", $this->health_cache, 300);
|
||||
|
||||
return ["healthy" => $healthy, "details" => $response];
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Agent Management
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Ensure an agent exists. Creates if not found.
|
||||
*
|
||||
* @param string $agent_id Agent identifier (e.g. "wp-user-1" or "wp-post-42").
|
||||
* @return bool True on success.
|
||||
*/
|
||||
public function ensure_agent($agent_id)
|
||||
{
|
||||
if (!$this->is_configured()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if agent already exists.
|
||||
$agent = $this->get("/api/v2/agents/{$agent_id}");
|
||||
if (!is_wp_error($agent) && !empty($agent["agent_id"])) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Create agent.
|
||||
$result = $this->post("/api/v2/agents", [
|
||||
"agent_id" => $agent_id,
|
||||
"pattern" => "support",
|
||||
"description" => "WP Agentic Writer agent",
|
||||
]);
|
||||
|
||||
return !is_wp_error($result);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Session Management
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Activate a session for an agent. Returns cached token if still valid.
|
||||
*
|
||||
* @param string $agent_id Agent identifier.
|
||||
* @return string|false Session token or false on failure.
|
||||
*/
|
||||
public function activate_session($agent_id)
|
||||
{
|
||||
if (!$this->is_configured()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$transient_key = "wpaw_memanto_token_" . md5($agent_id);
|
||||
$cached_token = get_transient($transient_key);
|
||||
|
||||
if (!empty($cached_token)) {
|
||||
return $cached_token;
|
||||
}
|
||||
|
||||
$response = $this->post(
|
||||
"/api/v2/agents/{$agent_id}/activate",
|
||||
[],
|
||||
$agent_id,
|
||||
);
|
||||
|
||||
if (is_wp_error($response) || empty($response["session_token"])) {
|
||||
wpaw_debug_log("MEMANTO activate_session failed", [
|
||||
"agent_id" => $agent_id,
|
||||
]);
|
||||
return false;
|
||||
}
|
||||
|
||||
$token = $response["session_token"];
|
||||
$expires_at = strtotime($response["expires_at"] ?? "+6 hours");
|
||||
$ttl = max(60, $expires_at - time() - 300); // Expire 5 min before actual.
|
||||
|
||||
set_transient($transient_key, $token, $ttl);
|
||||
return $token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deactivate a session for an agent.
|
||||
*
|
||||
* Clears the cached token and sends the deactivate request
|
||||
* without injecting a session token (avoids re-activation loop).
|
||||
*
|
||||
* @param string $agent_id Agent identifier.
|
||||
* @return bool True on success.
|
||||
*/
|
||||
public function deactivate_session($agent_id)
|
||||
{
|
||||
$transient_key = "wpaw_memanto_token_" . md5($agent_id);
|
||||
delete_transient($transient_key);
|
||||
|
||||
if (!$this->is_configured()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$url = $this->base_url . "/api/v2/agents/{$agent_id}/deactivate";
|
||||
$headers = $this->get_headers();
|
||||
|
||||
$response = wp_remote_post($url, [
|
||||
"headers" => $headers,
|
||||
"body" => wp_json_encode([]),
|
||||
"timeout" => 10,
|
||||
]);
|
||||
|
||||
if (is_wp_error($response)) {
|
||||
wpaw_debug_log("MEMANTO deactivate_session failed", [
|
||||
"agent_id" => $agent_id,
|
||||
]);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Memory Operations
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Store a memory.
|
||||
*
|
||||
* @param string $agent_id Agent identifier.
|
||||
* @param string $content Memory content (max 10000 chars).
|
||||
* @param string $type Memory type (fact, preference, goal, decision, artifact, learning, event, instruction, relationship, context, observation, commitment, error).
|
||||
* @param array $tags Optional tags.
|
||||
* @param string $title Optional title (max 100 chars).
|
||||
* @return bool True on success.
|
||||
*/
|
||||
public function remember(
|
||||
$agent_id,
|
||||
$content,
|
||||
$type = "context",
|
||||
$tags = [],
|
||||
$title = "",
|
||||
) {
|
||||
if (!$this->is_active()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$body = [
|
||||
"content" => mb_substr($content, 0, 10000),
|
||||
"type" => $type,
|
||||
"tags" => $tags,
|
||||
"source" => "wp-agentic-writer",
|
||||
];
|
||||
|
||||
if (!empty($title)) {
|
||||
$body["title"] = mb_substr($title, 0, 100);
|
||||
}
|
||||
|
||||
$response = $this->post(
|
||||
"/api/v2/agents/{$agent_id}/remember",
|
||||
$body,
|
||||
$agent_id,
|
||||
);
|
||||
|
||||
if (is_wp_error($response)) {
|
||||
wpaw_debug_log("MEMANTO remember failed", [
|
||||
"agent_id" => $agent_id,
|
||||
"type" => $type,
|
||||
"error" => $response->get_error_message(),
|
||||
]);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Store multiple memories in batch (max 100).
|
||||
*
|
||||
* @param string $agent_id Agent identifier.
|
||||
* @param array $memories Array of memory items. Each item: { content, type, tags, title }.
|
||||
* @return bool True on success.
|
||||
*/
|
||||
public function batch_remember($agent_id, $memories)
|
||||
{
|
||||
if (!$this->is_active() || empty($memories)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$batch = [];
|
||||
foreach (array_slice($memories, 0, 100) as $item) {
|
||||
$entry = [
|
||||
"content" => mb_substr($item["content"] ?? "", 0, 10000),
|
||||
"type" => $item["type"] ?? "context",
|
||||
"source" => "wp-agentic-writer",
|
||||
];
|
||||
if (!empty($item["title"])) {
|
||||
$entry["title"] = mb_substr($item["title"], 0, 100);
|
||||
}
|
||||
if (!empty($item["tags"])) {
|
||||
$entry["tags"] = $item["tags"];
|
||||
}
|
||||
$batch[] = $entry;
|
||||
}
|
||||
|
||||
$response = $this->post(
|
||||
"/api/v2/agents/{$agent_id}/batch-remember",
|
||||
["memories" => $batch],
|
||||
$agent_id,
|
||||
);
|
||||
|
||||
if (is_wp_error($response)) {
|
||||
wpaw_debug_log("MEMANTO batch_remember failed", [
|
||||
"agent_id" => $agent_id,
|
||||
"count" => count($batch),
|
||||
"error" => $response->get_error_message(),
|
||||
]);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Semantic search / recall memories.
|
||||
*
|
||||
* @param string $agent_id Agent identifier.
|
||||
* @param string $query Search query.
|
||||
* @param array $type_filter Optional memory type filter.
|
||||
* @param int $limit Max results (default 10).
|
||||
* @param float $min_similarity Minimum similarity score 0-1.
|
||||
* @return array Recalled memories (empty on failure).
|
||||
*/
|
||||
public function recall(
|
||||
$agent_id,
|
||||
$query,
|
||||
$type_filter = [],
|
||||
$limit = 10,
|
||||
$min_similarity = 0.3,
|
||||
) {
|
||||
if (!$this->is_active()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$body = [
|
||||
"query" => $query,
|
||||
"limit" => $limit,
|
||||
"min_similarity" => $min_similarity,
|
||||
];
|
||||
|
||||
if (!empty($type_filter)) {
|
||||
$body["type"] = $type_filter;
|
||||
}
|
||||
|
||||
$response = $this->post(
|
||||
"/api/v2/agents/{$agent_id}/recall",
|
||||
$body,
|
||||
$agent_id,
|
||||
);
|
||||
|
||||
if (is_wp_error($response)) {
|
||||
wpaw_debug_log("MEMANTO recall failed", [
|
||||
"agent_id" => $agent_id,
|
||||
"query" => substr($query, 0, 100),
|
||||
"error" => $response->get_error_message(),
|
||||
]);
|
||||
return [];
|
||||
}
|
||||
|
||||
return is_array($response) ? $response : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Recall most recent memories.
|
||||
*
|
||||
* @param string $agent_id Agent identifier.
|
||||
* @param int $limit Max results.
|
||||
* @param array $type_filter Optional memory type filter.
|
||||
* @return array Recent memories (empty on failure).
|
||||
*/
|
||||
public function recall_recent($agent_id, $limit = 10, $type_filter = [])
|
||||
{
|
||||
if (!$this->is_active()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$body = ["limit" => $limit];
|
||||
if (!empty($type_filter)) {
|
||||
$body["type"] = $type_filter;
|
||||
}
|
||||
|
||||
$response = $this->post(
|
||||
"/api/v2/agents/{$agent_id}/recall/recent",
|
||||
$body,
|
||||
$agent_id,
|
||||
);
|
||||
|
||||
if (is_wp_error($response)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return is_array($response) ? $response : [];
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Agent ID Builders
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Build user-level agent ID.
|
||||
*
|
||||
* @param int $user_id WordPress user ID.
|
||||
* @return string Agent ID like "wp-user-1".
|
||||
*/
|
||||
public function get_user_agent_id($user_id)
|
||||
{
|
||||
return "wp-user-" . absint($user_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build post-level agent ID.
|
||||
*
|
||||
* @param int $post_id WordPress post ID.
|
||||
* @return string Agent ID like "wp-post-42".
|
||||
*/
|
||||
public function get_post_agent_id($post_id)
|
||||
{
|
||||
return "wp-post-" . absint($post_id);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// HTTP Helpers (private)
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Get the Moorcheh API key from settings.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function get_moorcheh_key()
|
||||
{
|
||||
$settings = get_option("wp_agentic_writer_settings", []);
|
||||
return $settings["memanto_moorcheh_key"] ?? "";
|
||||
}
|
||||
|
||||
/**
|
||||
* GET request to MEMANTO API.
|
||||
*
|
||||
* @param string $path API path (e.g. "/health", "/api/v2/agents/{id}").
|
||||
* @return array|WP_Error Decoded response or error.
|
||||
*/
|
||||
private function get($path)
|
||||
{
|
||||
$url = $this->base_url . $path;
|
||||
|
||||
$response = wp_remote_get($url, [
|
||||
"headers" => $this->get_headers(),
|
||||
"timeout" => 10,
|
||||
]);
|
||||
|
||||
return $this->parse_response($response);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST request to MEMANTO API.
|
||||
*
|
||||
* @param string $path API path.
|
||||
* @param array $body Request body.
|
||||
* @param string $agent_id Optional agent ID for session token injection.
|
||||
* @return array|WP_Error Decoded response or error.
|
||||
*/
|
||||
private function post($path, $body = [], $agent_id = "")
|
||||
{
|
||||
$url = $this->base_url . $path;
|
||||
$headers = $this->get_headers();
|
||||
|
||||
// Inject session token if we have an agent_id.
|
||||
if (!empty($agent_id)) {
|
||||
$token = $this->activate_session($agent_id);
|
||||
if ($token) {
|
||||
$headers["X-Session-Token"] = $token;
|
||||
}
|
||||
}
|
||||
|
||||
$response = wp_remote_post($url, [
|
||||
"headers" => $headers,
|
||||
"body" => wp_json_encode($body),
|
||||
"timeout" => 10,
|
||||
]);
|
||||
|
||||
// Handle expired token (401) — re-activate and retry once.
|
||||
if (!is_wp_error($response)) {
|
||||
$code = wp_remote_retrieve_response_code($response);
|
||||
if (401 === $code && !empty($agent_id)) {
|
||||
// Clear cached token and retry.
|
||||
delete_transient("wpaw_memanto_token_" . md5($agent_id));
|
||||
$token = $this->activate_session($agent_id);
|
||||
if ($token) {
|
||||
$headers["X-Session-Token"] = $token;
|
||||
$response = wp_remote_post($url, [
|
||||
"headers" => $headers,
|
||||
"body" => wp_json_encode($body),
|
||||
"timeout" => 10,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $this->parse_response($response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build common headers for MEMANTO API requests.
|
||||
*
|
||||
* @return array Headers.
|
||||
*/
|
||||
private function get_headers()
|
||||
{
|
||||
return [
|
||||
"Content-Type" => "application/json",
|
||||
"Accept" => "application/json",
|
||||
"X-API-Key" => $this->get_moorcheh_key(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse HTTP response from MEMANTO API.
|
||||
*
|
||||
* @param array|WP_Error $response wp_remote response.
|
||||
* @return array|WP_Error Decoded body or error.
|
||||
*/
|
||||
private function parse_response($response)
|
||||
{
|
||||
if (is_wp_error($response)) {
|
||||
return new WP_Error(
|
||||
"memanto_connection_error",
|
||||
$response->get_error_message(),
|
||||
);
|
||||
}
|
||||
|
||||
$code = wp_remote_retrieve_response_code($response);
|
||||
$body = wp_remote_retrieve_body($response);
|
||||
|
||||
if (401 === $code) {
|
||||
// Let caller handle re-auth.
|
||||
return new WP_Error(
|
||||
"memanto_unauthorized",
|
||||
"Session token expired",
|
||||
);
|
||||
}
|
||||
|
||||
if ($code >= 400) {
|
||||
wpaw_debug_log("MEMANTO API error ({$code})", $body);
|
||||
return new WP_Error(
|
||||
"memanto_api_error",
|
||||
sprintf(
|
||||
"MEMANTO API error (%d): %s",
|
||||
$code,
|
||||
substr($body, 0, 200),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
$decoded = json_decode($body, true);
|
||||
return is_array($decoded) ? $decoded : [];
|
||||
}
|
||||
}
|
||||
754
includes/class-memanto-context-enhancer.php
Normal file
754
includes/class-memanto-context-enhancer.php
Normal file
@@ -0,0 +1,754 @@
|
||||
<?php
|
||||
/**
|
||||
* MEMANTO Context Enhancer
|
||||
*
|
||||
* Orchestrates when and what to remember/recall from MEMANTO.
|
||||
* Hooks into the existing Context Service to provide memory
|
||||
* enrichment without replacing any existing behavior.
|
||||
*
|
||||
* @package WP_Agentic_Writer
|
||||
* @since 0.3.0
|
||||
*/
|
||||
|
||||
if (!defined("ABSPATH")) {
|
||||
exit();
|
||||
}
|
||||
|
||||
/**
|
||||
* Class WP_Agentic_Writer_Memanto_Context_Enhancer
|
||||
*
|
||||
* Every public method checks is_active() first and returns gracefully
|
||||
* if MEMANTO is not available. The plugin never breaks.
|
||||
*/
|
||||
class WP_Agentic_Writer_Memanto_Context_Enhancer
|
||||
{
|
||||
/**
|
||||
* Singleton instance.
|
||||
*
|
||||
* @var WP_Agentic_Writer_Memanto_Context_Enhancer
|
||||
*/
|
||||
private static $instance = null;
|
||||
|
||||
/**
|
||||
* MEMANTO client reference.
|
||||
*
|
||||
* @var WP_Agentic_Writer_Memanto_Client
|
||||
*/
|
||||
private $client;
|
||||
|
||||
/**
|
||||
* Get singleton instance.
|
||||
*
|
||||
* @return WP_Agentic_Writer_Memanto_Context_Enhancer
|
||||
*/
|
||||
public static function get_instance()
|
||||
{
|
||||
if (null === self::$instance) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* Registers all WordPress action/filter hooks so that the sidebar
|
||||
* handlers only need a single do_action() call at each trigger point.
|
||||
*/
|
||||
private function __construct()
|
||||
{
|
||||
$this->client = WP_Agentic_Writer_Memanto_Client::get_instance();
|
||||
|
||||
// Session lifecycle.
|
||||
add_action(
|
||||
"wpaw_memanto_session_start",
|
||||
[$this, "on_session_start"],
|
||||
10,
|
||||
3,
|
||||
);
|
||||
add_action(
|
||||
"wpaw_memanto_session_end",
|
||||
[$this, "on_session_end"],
|
||||
10,
|
||||
2,
|
||||
);
|
||||
|
||||
// Write-through: remember on meaningful events.
|
||||
add_action(
|
||||
"wpaw_memanto_user_message",
|
||||
[$this, "on_user_message"],
|
||||
10,
|
||||
3,
|
||||
);
|
||||
add_action(
|
||||
"wpaw_memanto_plan_generated",
|
||||
[$this, "on_plan_generated"],
|
||||
10,
|
||||
2,
|
||||
);
|
||||
add_action(
|
||||
"wpaw_memanto_plan_approved",
|
||||
[$this, "on_plan_approved"],
|
||||
10,
|
||||
2,
|
||||
);
|
||||
add_action(
|
||||
"wpaw_memanto_plan_rejected",
|
||||
[$this, "on_plan_rejected"],
|
||||
10,
|
||||
2,
|
||||
);
|
||||
add_action(
|
||||
"wpaw_memanto_section_written",
|
||||
[$this, "on_section_written"],
|
||||
10,
|
||||
3,
|
||||
);
|
||||
add_action(
|
||||
"wpaw_memanto_block_refined",
|
||||
[$this, "on_block_refined"],
|
||||
10,
|
||||
3,
|
||||
);
|
||||
add_action(
|
||||
"wpaw_memanto_config_saved",
|
||||
[$this, "on_config_saved"],
|
||||
10,
|
||||
2,
|
||||
);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Session Lifecycle
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Called when a conversation session starts.
|
||||
* Ensures agents exist and recalls previous session state.
|
||||
*
|
||||
* @param string $session_id Session ID.
|
||||
* @param int $post_id Post ID.
|
||||
* @param int $user_id WordPress user ID.
|
||||
*/
|
||||
public function on_session_start($session_id, $post_id, $user_id)
|
||||
{
|
||||
if (!$this->client->is_active()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure user agent exists.
|
||||
if ($user_id > 0) {
|
||||
$this->client->ensure_agent(
|
||||
$this->client->get_user_agent_id($user_id),
|
||||
);
|
||||
}
|
||||
|
||||
// Ensure post agent exists.
|
||||
if ($post_id > 0) {
|
||||
$this->client->ensure_agent(
|
||||
$this->client->get_post_agent_id($post_id),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a conversation session ends.
|
||||
* Stores a session summary memory and deactivates the session.
|
||||
*
|
||||
* @param string $session_id Session ID.
|
||||
* @param int $post_id Post ID.
|
||||
*/
|
||||
public function on_session_end($session_id, $post_id)
|
||||
{
|
||||
if (!$this->client->is_active() || $post_id <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$post_agent = $this->client->get_post_agent_id($post_id);
|
||||
|
||||
// Store a session summary.
|
||||
$this->client->remember(
|
||||
$post_agent,
|
||||
"Session ended: " . $session_id,
|
||||
"context",
|
||||
["session:" . $session_id, "post:" . $post_id],
|
||||
"Session end",
|
||||
);
|
||||
|
||||
// Deactivate MEMANTO session to trigger summary generation.
|
||||
$this->client->deactivate_session($post_agent);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Write-Through: Remember on Meaningful Events
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Store a memory when user sends a chat message.
|
||||
*
|
||||
* @param string $session_id Session ID.
|
||||
* @param string $content User message content.
|
||||
* @param int $post_id Post ID.
|
||||
*/
|
||||
public function on_user_message($session_id, $content, $post_id)
|
||||
{
|
||||
if (!$this->client->is_active() || $post_id <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->client->remember(
|
||||
$this->client->get_post_agent_id($post_id),
|
||||
"User instruction: " . wp_strip_all_tags($content),
|
||||
"instruction",
|
||||
["post:" . $post_id, "session:" . $session_id],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a memory when a plan is generated.
|
||||
*
|
||||
* @param int $post_id Post ID.
|
||||
* @param array $plan Plan data.
|
||||
*/
|
||||
public function on_plan_generated($post_id, $plan)
|
||||
{
|
||||
if (!$this->client->is_active() || $post_id <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$title = $plan["title"] ?? "Untitled";
|
||||
$sections =
|
||||
isset($plan["sections"]) && is_array($plan["sections"])
|
||||
? count($plan["sections"])
|
||||
: 0;
|
||||
|
||||
$this->client->remember(
|
||||
$this->client->get_post_agent_id($post_id),
|
||||
sprintf('Plan generated: "%s" with %d sections', $title, $sections),
|
||||
"artifact",
|
||||
["post:" . $post_id, "type:plan"],
|
||||
"Plan: " . $title,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a memory when user approves a plan.
|
||||
*
|
||||
* @param int $post_id Post ID.
|
||||
* @param array $plan Plan data.
|
||||
*/
|
||||
public function on_plan_approved($post_id, $plan)
|
||||
{
|
||||
if (!$this->client->is_active() || $post_id <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$title = $plan["title"] ?? "Untitled";
|
||||
|
||||
$this->client->remember(
|
||||
$this->client->get_post_agent_id($post_id),
|
||||
sprintf('User approved plan: "%s"', $title),
|
||||
"decision",
|
||||
["post:" . $post_id, "type:plan"],
|
||||
"Plan approved",
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a memory when user rejects or requests plan changes.
|
||||
*
|
||||
* @param int $post_id Post ID.
|
||||
* @param string $reason Rejection/revision reason.
|
||||
*/
|
||||
public function on_plan_rejected($post_id, $reason)
|
||||
{
|
||||
if (!$this->client->is_active() || $post_id <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->client->remember(
|
||||
$this->client->get_post_agent_id($post_id),
|
||||
"User requested plan revision: " . wp_strip_all_tags($reason),
|
||||
"error",
|
||||
["post:" . $post_id, "type:plan"],
|
||||
"Plan revision",
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a memory when a section is written.
|
||||
*
|
||||
* @param int $post_id Post ID.
|
||||
* @param string $section_id Section identifier.
|
||||
* @param string $summary Brief section summary.
|
||||
*/
|
||||
public function on_section_written($post_id, $section_id, $summary)
|
||||
{
|
||||
if (!$this->client->is_active() || $post_id <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->client->remember(
|
||||
$this->client->get_post_agent_id($post_id),
|
||||
"Section written (" .
|
||||
$section_id .
|
||||
"): " .
|
||||
wp_strip_all_tags($summary),
|
||||
"artifact",
|
||||
["post:" . $post_id, "section:" . $section_id],
|
||||
"Section: " . $section_id,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a memory when a block is refined.
|
||||
*
|
||||
* @param int $post_id Post ID.
|
||||
* @param string $block_id Block identifier.
|
||||
* @param string $instruction Refinement instruction.
|
||||
*/
|
||||
public function on_block_refined($post_id, $block_id, $instruction)
|
||||
{
|
||||
if (!$this->client->is_active() || $post_id <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->client->remember(
|
||||
$this->client->get_post_agent_id($post_id),
|
||||
"Block refined (" .
|
||||
$block_id .
|
||||
"): " .
|
||||
wp_strip_all_tags($instruction),
|
||||
"instruction",
|
||||
["post:" . $post_id, "block:" . $block_id],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a memory when post config is saved.
|
||||
*
|
||||
* @param int $post_id Post ID.
|
||||
* @param array $config Post config data.
|
||||
*/
|
||||
public function on_config_saved($post_id, $config)
|
||||
{
|
||||
if (!$this->client->is_active()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$config_summary = sprintf(
|
||||
"tone=%s, audience=%s, length=%s, language=%s",
|
||||
$config["tone"] ?? "default",
|
||||
$config["audience"] ?? "general",
|
||||
$config["article_length"] ?? "medium",
|
||||
$config["language"] ?? "auto",
|
||||
);
|
||||
|
||||
// Store to post agent.
|
||||
if ($post_id > 0) {
|
||||
$this->client->remember(
|
||||
$this->client->get_post_agent_id($post_id),
|
||||
"Article config: " . $config_summary,
|
||||
"preference",
|
||||
["post:" . $post_id],
|
||||
"Post config",
|
||||
);
|
||||
}
|
||||
|
||||
// Store to user agent (cross-post preferences).
|
||||
$user_id = get_current_user_id();
|
||||
if ($user_id > 0) {
|
||||
$this->client->remember(
|
||||
$this->client->get_user_agent_id($user_id),
|
||||
"User preference: " . $config_summary,
|
||||
"preference",
|
||||
["user:" . $user_id],
|
||||
"Writing preferences",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Recall: Retrieve Memories for Context Enrichment
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Recall relevant memories to enrich AI prompt context.
|
||||
*
|
||||
* @param int $post_id Post ID.
|
||||
* @param int $user_id WordPress user ID.
|
||||
* @param string $current_message User's current message (for semantic search).
|
||||
* @return array Recalled memory items, each with 'type', 'content', 'title'.
|
||||
*/
|
||||
public function recall_for_context(
|
||||
$post_id,
|
||||
$user_id,
|
||||
$current_message = "",
|
||||
) {
|
||||
if (!$this->client->is_active()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$memories = [];
|
||||
$seen = [];
|
||||
|
||||
// 1. Recent post memories.
|
||||
if ($post_id > 0) {
|
||||
$post_agent = $this->client->get_post_agent_id($post_id);
|
||||
$recent = $this->client->recall_recent($post_agent, 10);
|
||||
|
||||
foreach ($this->normalize_memories($recent) as $item) {
|
||||
$hash = md5($item["content"]);
|
||||
if (!isset($seen[$hash])) {
|
||||
$seen[$hash] = true;
|
||||
$memories[] = $item;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Semantic recall based on current message.
|
||||
if (!empty($current_message)) {
|
||||
$semantic = $this->client->recall(
|
||||
$post_agent,
|
||||
$current_message,
|
||||
[],
|
||||
5,
|
||||
);
|
||||
foreach ($this->normalize_memories($semantic) as $item) {
|
||||
$hash = md5($item["content"]);
|
||||
if (!isset($seen[$hash])) {
|
||||
$seen[$hash] = true;
|
||||
$memories[] = $item;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. User preferences (cross-post).
|
||||
if ($user_id > 0) {
|
||||
$user_prefs = $this->client->recall(
|
||||
$this->client->get_user_agent_id($user_id),
|
||||
"writing preferences tone audience language",
|
||||
["preference"],
|
||||
5,
|
||||
);
|
||||
|
||||
foreach ($this->normalize_memories($user_prefs) as $item) {
|
||||
$hash = md5($item["content"]);
|
||||
if (!isset($seen[$hash])) {
|
||||
$seen[$hash] = true;
|
||||
$memories[] = $item;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $memories;
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore session state from MEMANTO when reopening a post.
|
||||
*
|
||||
* Returns a structured restore payload that the frontend can use to:
|
||||
* - Display a "Restored from memory" badge
|
||||
* - Build a restored-session system message for the AI
|
||||
* - Pre-fill config from prior preferences
|
||||
*
|
||||
* @since 0.4.0
|
||||
* @param int $post_id Post ID being reopened.
|
||||
* @param int $user_id Current user ID.
|
||||
* @return array { restored: bool, memories: array, preferences: array, summary: string }
|
||||
*/
|
||||
public function restore_session($post_id, $user_id)
|
||||
{
|
||||
$empty = [
|
||||
"restored" => false,
|
||||
"memories" => [],
|
||||
"preferences" => [],
|
||||
"summary" => "",
|
||||
];
|
||||
|
||||
if (!$this->client->is_active() || $post_id <= 0) {
|
||||
return $empty;
|
||||
}
|
||||
|
||||
// Recall recent post memories (no current message — restore is about history).
|
||||
$post_agent = $this->client->get_post_agent_id($post_id);
|
||||
$recent = $this->client->recall_recent($post_agent, 15);
|
||||
$memories = $this->normalize_memories($recent);
|
||||
|
||||
// Recall user preferences (for cross-post config carry-over).
|
||||
$preferences = [];
|
||||
if ($user_id > 0) {
|
||||
$user_prefs = $this->client->recall(
|
||||
$this->client->get_user_agent_id($user_id),
|
||||
"writing preferences tone audience language length",
|
||||
["preference"],
|
||||
5,
|
||||
);
|
||||
$preferences = $this->normalize_memories($user_prefs);
|
||||
}
|
||||
|
||||
if (empty($memories) && empty($preferences)) {
|
||||
return $empty;
|
||||
}
|
||||
|
||||
// Build a human-readable summary for the restore badge tooltip.
|
||||
$summary = $this->build_restore_summary($memories, $preferences);
|
||||
|
||||
return [
|
||||
"restored" => true,
|
||||
"memories" => $memories,
|
||||
"preferences" => $preferences,
|
||||
"summary" => $summary,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a compact summary string of restored memories.
|
||||
*
|
||||
* @param array $memories Restored memories.
|
||||
* @param array $preferences Restored preferences.
|
||||
* @return string Summary text.
|
||||
*/
|
||||
private function build_restore_summary($memories, $preferences)
|
||||
{
|
||||
$parts = [];
|
||||
|
||||
if (!empty($memories)) {
|
||||
$parts[] = sprintf(
|
||||
/* translators: %d: number of memories */
|
||||
_n(
|
||||
"%d memory",
|
||||
"%d memories",
|
||||
count($memories),
|
||||
"wp-agentic-writer",
|
||||
),
|
||||
count($memories),
|
||||
);
|
||||
}
|
||||
|
||||
if (!empty($preferences)) {
|
||||
$parts[] = sprintf(
|
||||
/* translators: %d: number of preferences */
|
||||
_n(
|
||||
"%d preference",
|
||||
"%d preferences",
|
||||
count($preferences),
|
||||
"wp-agentic-writer",
|
||||
),
|
||||
count($preferences),
|
||||
);
|
||||
}
|
||||
|
||||
return implode(", ", $parts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user's writing preferences recalled from MEMANTO.
|
||||
*
|
||||
* Used when creating a new post to pre-fill the post config
|
||||
* with the user's habitual tone, audience, etc.
|
||||
*
|
||||
* @since 0.4.0
|
||||
* @param int $user_id WordPress user ID.
|
||||
* @return array { restored: bool, config: array }
|
||||
*/
|
||||
public function get_user_preferences_for_new_post($user_id)
|
||||
{
|
||||
$empty = ["restored" => false, "config" => []];
|
||||
|
||||
if (!$this->client->is_active() || $user_id <= 0) {
|
||||
return $empty;
|
||||
}
|
||||
|
||||
$user_prefs = $this->client->recall(
|
||||
$this->client->get_user_agent_id($user_id),
|
||||
"writing preferences tone audience language length",
|
||||
["preference"],
|
||||
3,
|
||||
);
|
||||
|
||||
$prefs = $this->normalize_memories($user_prefs);
|
||||
if (empty($prefs)) {
|
||||
return $empty;
|
||||
}
|
||||
|
||||
// Extract config fields from preference content.
|
||||
// Memory format: "User preference: tone=professional, audience=experts, length=long, language=en"
|
||||
$config = $this->extract_config_from_preferences($prefs);
|
||||
|
||||
if (empty($config)) {
|
||||
return $empty;
|
||||
}
|
||||
|
||||
return ["restored" => true, "config" => $config];
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse preference memory contents and extract config fields.
|
||||
*
|
||||
* @param array $prefs Normalized preference memories.
|
||||
* @return array Extracted config: { tone, audience, article_length, language }.
|
||||
*/
|
||||
private function extract_config_from_preferences($prefs)
|
||||
{
|
||||
$config = [];
|
||||
|
||||
// Combine all preference content into one blob for parsing.
|
||||
$blob = "";
|
||||
foreach ($prefs as $pref) {
|
||||
$blob .= " " . ($pref["content"] ?? "");
|
||||
}
|
||||
|
||||
// Match key=value patterns.
|
||||
if (preg_match('/tone\s*=\s*([^,\n]+)/i', $blob, $m)) {
|
||||
$val = trim($m[1]);
|
||||
if ("" !== $val && "default" !== strtolower($val)) {
|
||||
$config["tone"] = $val;
|
||||
}
|
||||
}
|
||||
if (preg_match('/audience\s*=\s*([^,\n]+)/i', $blob, $m)) {
|
||||
$val = trim($m[1]);
|
||||
if ("" !== $val && "general" !== strtolower($val)) {
|
||||
$config["audience"] = $val;
|
||||
}
|
||||
}
|
||||
if (preg_match('/(?:article_)?length\s*=\s*([^,\n]+)/i', $blob, $m)) {
|
||||
$val = trim($m[1]);
|
||||
if ("" !== $val && "medium" !== strtolower($val)) {
|
||||
$config["article_length"] = $val;
|
||||
}
|
||||
}
|
||||
if (preg_match('/language\s*=\s*([^,\n]+)/i', $blob, $m)) {
|
||||
$val = trim($m[1]);
|
||||
if ("" !== $val && "auto" !== strtolower($val)) {
|
||||
$config["language"] = $val;
|
||||
}
|
||||
}
|
||||
|
||||
return $config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a "restored session" system message for the AI.
|
||||
*
|
||||
* This message summarizes prior post work so the AI can resume
|
||||
* mid-article without re-asking the user for context.
|
||||
*
|
||||
* @since 0.4.0
|
||||
* @param array $restore_payload Result from restore_session().
|
||||
* @return string System message content (empty if nothing to restore).
|
||||
*/
|
||||
public function build_session_restore_message($restore_payload)
|
||||
{
|
||||
if (empty($restore_payload["restored"])) {
|
||||
return "";
|
||||
}
|
||||
|
||||
$memories = $restore_payload["memories"] ?? [];
|
||||
$preferences = $restore_payload["preferences"] ?? [];
|
||||
|
||||
if (empty($memories) && empty($preferences)) {
|
||||
return "";
|
||||
}
|
||||
|
||||
$lines = ["SESSION RESTORED FROM MEMORY"];
|
||||
$lines[] =
|
||||
"The user has returned to this post. Below is context from prior sessions.";
|
||||
$lines[] =
|
||||
"Use this to continue where the conversation left off without re-asking.";
|
||||
|
||||
// Summarize prior post work.
|
||||
if (!empty($memories)) {
|
||||
$lines[] = "";
|
||||
$lines[] = "## Prior session activity";
|
||||
|
||||
$grouped = [];
|
||||
foreach ($memories as $memory) {
|
||||
$type = $memory["type"] ?? "context";
|
||||
if (!isset($grouped[$type])) {
|
||||
$grouped[$type] = [];
|
||||
}
|
||||
$grouped[$type][] = $memory;
|
||||
}
|
||||
|
||||
// Render in priority order: plan > decision > instruction > artifact > error > context.
|
||||
$priority = [
|
||||
"artifact",
|
||||
"decision",
|
||||
"instruction",
|
||||
"error",
|
||||
"learning",
|
||||
"context",
|
||||
"preference",
|
||||
];
|
||||
foreach ($priority as $type) {
|
||||
if (empty($grouped[$type])) {
|
||||
continue;
|
||||
}
|
||||
$type_label = ucfirst($type) . "s";
|
||||
$lines[] = "### {$type_label}";
|
||||
foreach ($grouped[$type] as $m) {
|
||||
$content = trim($m["content"] ?? "");
|
||||
if ("" === $content) {
|
||||
continue;
|
||||
}
|
||||
$title = !empty($m["title"])
|
||||
? " [" . trim($m["title"]) . "]"
|
||||
: "";
|
||||
$lines[] = "- {$content}{$title}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Append user preferences (compact).
|
||||
if (!empty($preferences)) {
|
||||
$lines[] = "";
|
||||
$lines[] = "## User writing preferences";
|
||||
foreach ($preferences as $pref) {
|
||||
$content = trim($pref["content"] ?? "");
|
||||
if ("" !== $content) {
|
||||
$lines[] = "- {$content}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return implode("\n", $lines);
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize raw MEMANTO response into a uniform array.
|
||||
*
|
||||
* @param array $raw Raw response from recall/recall_recent.
|
||||
* @return array Normalized items: { type, content, title }.
|
||||
*/
|
||||
private function normalize_memories($raw)
|
||||
{
|
||||
if (!is_array($raw)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Handle different possible response shapes.
|
||||
$items = $raw;
|
||||
|
||||
// If response has a 'memories' or 'results' key, unwrap.
|
||||
if (isset($raw["memories"]) && is_array($raw["memories"])) {
|
||||
$items = $raw["memories"];
|
||||
} elseif (isset($raw["results"]) && is_array($raw["results"])) {
|
||||
$items = $raw["results"];
|
||||
}
|
||||
|
||||
$normalized = [];
|
||||
foreach ($items as $item) {
|
||||
if (!is_array($item)) {
|
||||
continue;
|
||||
}
|
||||
$normalized[] = [
|
||||
"type" => $item["type"] ?? "context",
|
||||
"content" => $item["content"] ?? ($item["text"] ?? ""),
|
||||
"title" => $item["title"] ?? "",
|
||||
];
|
||||
}
|
||||
|
||||
return $normalized;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -6,24 +6,24 @@
|
||||
* @var array $view_data Prepared view data from class-settings-v2.php
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
if (!defined("ABSPATH")) {
|
||||
exit();
|
||||
}
|
||||
|
||||
// Extract view data for easier access
|
||||
extract( $view_data );
|
||||
extract($view_data);
|
||||
?>
|
||||
<div class="wrap wpaw-settings-v2-wrap">
|
||||
<!-- Agentic IDE Split View Layout -->
|
||||
<div class="wpaw-ide-container d-flex">
|
||||
|
||||
|
||||
<!-- Left Sidebar: Settings Navigation -->
|
||||
<div class="wpaw-sidebar-nav flex-shrink-0">
|
||||
<!-- Header inside Sidebar -->
|
||||
<div class="wpaw-sidebar-header p-3 mb-2 border-bottom border-dark">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<img src="<?php echo esc_url( WP_AGENTIC_WRITER_URL . 'assets/img/icon.svg' ); ?>"
|
||||
alt="WP Agentic Writer"
|
||||
<img src="<?php echo esc_url(WP_AGENTIC_WRITER_URL . "assets/img/icon.svg"); ?>"
|
||||
alt="WP Agentic Writer"
|
||||
style="width: 24px; height: 24px; filter: invert(1)">
|
||||
<h1 class="h6 mb-0 text-white fw-bold">Agentic Writer</h1>
|
||||
</div>
|
||||
@@ -39,19 +39,25 @@ extract( $view_data );
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link active w-100 text-start d-flex align-items-center gap-2" id="general-tab" data-bs-toggle="pill" data-bs-target="#general" type="button" role="tab" aria-controls="general" aria-selected="true">
|
||||
<i class="bi bi-sliders"></i>
|
||||
<?php esc_html_e( 'General', 'wp-agentic-writer' ); ?>
|
||||
<?php esc_html_e("General", "wp-agentic-writer"); ?>
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link w-100 text-start d-flex align-items-center gap-2" id="models-tab" data-bs-toggle="pill" data-bs-target="#models" type="button" role="tab" aria-controls="models" aria-selected="false">
|
||||
<i class="bi bi-stars"></i>
|
||||
<?php esc_html_e( 'AI Models', 'wp-agentic-writer' ); ?>
|
||||
<?php esc_html_e("AI Models", "wp-agentic-writer"); ?>
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link w-100 text-start d-flex align-items-center gap-2" id="local-backend-tab" data-bs-toggle="pill" data-bs-target="#local-backend" type="button" role="tab" aria-controls="local-backend" aria-selected="false">
|
||||
<i class="bi bi-house-fill"></i>
|
||||
<?php esc_html_e( 'Local Backend', 'wp-agentic-writer' ); ?>
|
||||
<?php esc_html_e("Local Backend", "wp-agentic-writer"); ?>
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link w-100 text-start d-flex align-items-center gap-2" id="memanto-tab" data-bs-toggle="pill" data-bs-target="#memanto" type="button" role="tab" aria-controls="memanto" aria-selected="false">
|
||||
<i class="bi bi-cpu"></i>
|
||||
<?php esc_html_e("MEMANTO", "wp-agentic-writer"); ?>
|
||||
</button>
|
||||
</li>
|
||||
|
||||
@@ -59,13 +65,13 @@ extract( $view_data );
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link w-100 text-start d-flex align-items-center gap-2" id="cost-log-tab" data-bs-toggle="pill" data-bs-target="#cost-log" type="button" role="tab" aria-controls="cost-log" aria-selected="false">
|
||||
<i class="bi bi-graph-up"></i>
|
||||
<?php esc_html_e( 'OpenRouter Cost Log', 'wp-agentic-writer' ); ?>
|
||||
<?php esc_html_e("OpenRouter Cost Log", "wp-agentic-writer"); ?>
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link w-100 text-start d-flex align-items-center gap-2" id="guide-tab" data-bs-toggle="pill" data-bs-target="#guide" type="button" role="tab" aria-controls="guide" aria-selected="false">
|
||||
<i class="bi bi-book"></i>
|
||||
<?php esc_html_e( 'Model Guide', 'wp-agentic-writer' ); ?>
|
||||
<?php esc_html_e("Model Guide", "wp-agentic-writer"); ?>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -75,7 +81,7 @@ extract( $view_data );
|
||||
<!-- Right Content Pane: Settings Forms -->
|
||||
<div class="wpaw-content-pane flex-grow-1 d-flex flex-column h-100">
|
||||
<form method="post" action="options.php" id="wpaw-settings-form" class="h-100 d-flex flex-column">
|
||||
<?php settings_fields( 'wp_agentic_writer_settings' ); ?>
|
||||
<?php settings_fields("wp_agentic_writer_settings"); ?>
|
||||
|
||||
<!-- Workflow Pipeline Progress -->
|
||||
<div class="wpaw-workflow-progress wpaw-workflow-compact mb-4" id="wpaw-workflow-display">
|
||||
@@ -140,7 +146,7 @@ extract( $view_data );
|
||||
<h2 class="h4 text-white m-0">General Settings</h2>
|
||||
<p class="text-secondary small mt-1">Configure global API keys, budget, and content parameters.</p>
|
||||
</div>
|
||||
<?php include WP_AGENTIC_WRITER_DIR . 'views/settings/tab-general.php'; ?>
|
||||
<?php include WP_AGENTIC_WRITER_DIR . "views/settings/tab-general.php"; ?>
|
||||
</div>
|
||||
|
||||
<!-- Models Tab -->
|
||||
@@ -149,7 +155,7 @@ extract( $view_data );
|
||||
<h2 class="h4 text-white m-0">AI Models</h2>
|
||||
<p class="text-secondary small mt-1">Select logic engines for different stages of the writing pipeline.</p>
|
||||
</div>
|
||||
<?php include WP_AGENTIC_WRITER_DIR . 'views/settings/tab-models.php'; ?>
|
||||
<?php include WP_AGENTIC_WRITER_DIR . "views/settings/tab-models.php"; ?>
|
||||
</div>
|
||||
|
||||
<!-- Local Backend Tab -->
|
||||
@@ -158,16 +164,25 @@ extract( $view_data );
|
||||
<h2 class="h4 text-white m-0">Local Backend</h2>
|
||||
<p class="text-secondary small mt-1">Configure connections to local LM Studio or Ollama instances.</p>
|
||||
</div>
|
||||
<?php include WP_AGENTIC_WRITER_DIR . 'views/settings/tab-local-backend.php'; ?>
|
||||
<?php include WP_AGENTIC_WRITER_DIR . "views/settings/tab-local-backend.php"; ?>
|
||||
</div>
|
||||
|
||||
<!-- Cost Log Tab -->
|
||||
<!-- MEMANTO Tab -->
|
||||
<div class="tab-pane fade" id="memanto" role="tabpanel" aria-labelledby="memanto-tab">
|
||||
<div class="mb-4 pb-3 border-bottom border-dark">
|
||||
<h2 class="h4 text-white m-0">MEMANTO Context Keeper</h2>
|
||||
<p class="text-secondary small mt-1">Optional persistent memory for your AI writing assistant. The plugin works perfectly without it.</p>
|
||||
</div>
|
||||
<?php include WP_AGENTIC_WRITER_DIR . "views/settings/tab-memanto.php"; ?>
|
||||
</div>
|
||||
|
||||
<!-- Cost Log Tab -->
|
||||
<div class="tab-pane fade" id="cost-log" role="tabpanel" aria-labelledby="cost-log-tab">
|
||||
<div class="mb-4 pb-3 border-bottom border-dark">
|
||||
<h2 class="h4 text-white m-0">OpenRouter Cost Analytics</h2>
|
||||
<p class="text-secondary small mt-1">Track API token usage and expenses across all generations.</p>
|
||||
</div>
|
||||
<?php include WP_AGENTIC_WRITER_DIR . 'views/settings/tab-cost-log.php'; ?>
|
||||
<?php include WP_AGENTIC_WRITER_DIR . "views/settings/tab-cost-log.php"; ?>
|
||||
</div>
|
||||
|
||||
<!-- Guide Tab -->
|
||||
@@ -176,7 +191,7 @@ extract( $view_data );
|
||||
<h2 class="h4 text-white m-0">Provider Documentation</h2>
|
||||
<p class="text-secondary small mt-1">Reference materials for selecting the right model constraints.</p>
|
||||
</div>
|
||||
<?php include WP_AGENTIC_WRITER_DIR . 'views/settings/tab-guide.php'; ?>
|
||||
<?php include WP_AGENTIC_WRITER_DIR . "views/settings/tab-guide.php"; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -185,18 +200,28 @@ extract( $view_data );
|
||||
<div class="wpaw-save-bar p-3 border-top border-dark d-flex justify-content-between align-items-center bg-transparent mt-auto sticky-bottom">
|
||||
<div class="text-secondary small d-flex align-items-center gap-2">
|
||||
<span class="dashicons dashicons-plugin text-primary"></span>
|
||||
<?php printf( esc_html__( 'v%s', 'wp-agentic-writer' ), esc_html( WP_AGENTIC_WRITER_VERSION ) ); ?>
|
||||
<?php printf(
|
||||
esc_html__("v%s", "wp-agentic-writer"),
|
||||
esc_html(WP_AGENTIC_WRITER_VERSION),
|
||||
); ?>
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary" id="wpaw-reset-settings">
|
||||
<?php esc_html_e( 'Reset Defaults', 'wp-agentic-writer' ); ?>
|
||||
<?php esc_html_e("Reset Defaults", "wp-agentic-writer"); ?>
|
||||
</button>
|
||||
<button type="submit" class="btn btn-sm btn-primary px-4 fw-semibold" id="wpaw-save-settings">
|
||||
<?php
|
||||
$is_mac = isset( $_SERVER['HTTP_USER_AGENT'] ) && strpos( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ), 'Mac OS' ) !== false;
|
||||
$cmd_key = $is_mac ? '⌘' : 'Ctrl';
|
||||
?>
|
||||
<?php esc_html_e( 'Save Settings', 'wp-agentic-writer' ); ?> <kbd class="ms-1 bg-dark text-white border-0 py-0"><?php echo esc_html( $cmd_key ); ?>+S</kbd>
|
||||
$is_mac =
|
||||
isset($_SERVER["HTTP_USER_AGENT"]) &&
|
||||
strpos(wp_unslash($_SERVER["HTTP_USER_AGENT"]), "Mac OS") !== false;
|
||||
$cmd_key = $is_mac ? "⌘" : "Ctrl";
|
||||
?>
|
||||
<?php esc_html_e(
|
||||
"Save Settings",
|
||||
"wp-agentic-writer",
|
||||
); ?> <kbd class="ms-1 bg-dark text-white border-0 py-0"><?php echo esc_html(
|
||||
$cmd_key,
|
||||
); ?>+S</kbd>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -210,7 +235,10 @@ extract( $view_data );
|
||||
<div id="wpaw-toast" class="toast" role="alert" aria-live="assertive" aria-atomic="true">
|
||||
<div class="toast-header">
|
||||
<span class="me-2">✨</span>
|
||||
<strong class="me-auto"><?php esc_html_e( 'WP Agentic Writer', 'wp-agentic-writer' ); ?></strong>
|
||||
<strong class="me-auto"><?php esc_html_e(
|
||||
"WP Agentic Writer",
|
||||
"wp-agentic-writer",
|
||||
); ?></strong>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="toast-body" id="wpaw-toast-message"></div>
|
||||
|
||||
195
views/settings/tab-memanto.php
Normal file
195
views/settings/tab-memanto.php
Normal file
@@ -0,0 +1,195 @@
|
||||
<?php
|
||||
/**
|
||||
* Settings Tab: MEMANTO Context Keeper
|
||||
*
|
||||
* @package WP_Agentic_Writer
|
||||
* @since 0.3.0
|
||||
* @var array $view_data Prepared view data (extracted in layout.php)
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
// Variables from $view_data (extracted in layout.php).
|
||||
$memanto_enabled = $memanto_enabled ?? false;
|
||||
$memanto_url = $memanto_url ?? '';
|
||||
$memanto_moorcheh_key = $memanto_moorcheh_key ?? '';
|
||||
?>
|
||||
|
||||
<div class="row g-4">
|
||||
|
||||
<!-- Enable MEMANTO -->
|
||||
<div class="col-12">
|
||||
<div class="wpaw-card">
|
||||
<div class="wpaw-card-body">
|
||||
<div class="form-check form-switch mb-0">
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
role="switch"
|
||||
id="memanto_enabled"
|
||||
name="wp_agentic_writer_settings[memanto_enabled]"
|
||||
value="1"
|
||||
<?php checked( $memanto_enabled ); ?>
|
||||
>
|
||||
<label class="form-check-label text-white fw-medium" for="memanto_enabled">
|
||||
<?php esc_html_e( 'Enable MEMANTO Integration', 'wp-agentic-writer' ); ?>
|
||||
</label>
|
||||
</div>
|
||||
<p class="text-secondary small mt-2 mb-0">
|
||||
When enabled, the AI writing assistant will store and recall memories across sessions using MEMANTO.
|
||||
The plugin works perfectly without MEMANTO — this is an optional enhancement.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- MEMANTO Instance URL -->
|
||||
<div class="col-md-6">
|
||||
<div class="wpaw-card h-100">
|
||||
<div class="wpaw-card-body">
|
||||
<label for="memanto_url" class="form-label text-white fw-medium">
|
||||
<i class="bi bi-link-45deg me-1"></i>
|
||||
<?php esc_html_e( 'MEMANTO Instance URL', 'wp-agentic-writer' ); ?>
|
||||
</label>
|
||||
<input
|
||||
type="url"
|
||||
class="form-control"
|
||||
id="memanto_url"
|
||||
name="wp_agentic_writer_settings[memanto_url]"
|
||||
value="<?php echo esc_attr( $memanto_url ); ?>"
|
||||
placeholder="https://your-instance.context.wpagentic.dev"
|
||||
autocomplete="off"
|
||||
>
|
||||
<div class="form-text text-secondary">
|
||||
The URL of your MEMANTO instance. Provided when you subscribe to MEMANTO Context Keeper.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Moorcheh API Key -->
|
||||
<div class="col-md-6">
|
||||
<div class="wpaw-card h-100">
|
||||
<div class="wpaw-card-body">
|
||||
<label for="memanto_moorcheh_key" class="form-label text-white fw-medium">
|
||||
<i class="bi bi-key me-1"></i>
|
||||
<?php esc_html_e( 'Moorcheh API Key', 'wp-agentic-writer' ); ?>
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
class="form-control"
|
||||
id="memanto_moorcheh_key"
|
||||
name="wp_agentic_writer_settings[memanto_moorcheh_key]"
|
||||
value="<?php echo esc_attr( $memanto_moorcheh_key ); ?>"
|
||||
placeholder="Enter your Moorcheh API key"
|
||||
autocomplete="new-password"
|
||||
>
|
||||
<div class="form-text text-secondary">
|
||||
Get a free API key at <a href="https://moorcheh.ai" target="_blank" class="text-info">moorcheh.ai</a> (10K vectors/month included).
|
||||
Billed to your own Moorcheh account.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Connection Status & Test -->
|
||||
<div class="col-12">
|
||||
<div class="wpaw-card">
|
||||
<div class="wpaw-card-body">
|
||||
<div class="d-flex align-items-center justify-content-between flex-wrap gap-3">
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
<span id="memanto-status-indicator" class="badge rounded-pill bg-secondary">
|
||||
<?php if ( ! empty( $memanto_url ) && ! empty( $memanto_moorcheh_key ) ) : ?>
|
||||
<span class="spinner-border spinner-border-sm me-1" role="status"></span> Checking...
|
||||
<?php else : ?>
|
||||
Not configured
|
||||
<?php endif; ?>
|
||||
</span>
|
||||
<span id="memanto-status-detail" class="text-secondary small"></span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-outline-info"
|
||||
id="wpaw-test-memanto"
|
||||
<?php echo ( empty( $memanto_url ) || empty( $memanto_moorcheh_key ) ) ? 'disabled' : ''; ?>
|
||||
>
|
||||
<i class="bi bi-wifi me-1"></i>
|
||||
<?php esc_html_e( 'Test Connection', 'wp-agentic-writer' ); ?>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Info Box -->
|
||||
<div class="col-12">
|
||||
<div class="wpaw-card border-info" style="border-left: 3px solid var(--bs-info);">
|
||||
<div class="wpaw-card-body">
|
||||
<div class="d-flex gap-3">
|
||||
<span class="fs-4">🧠</span>
|
||||
<div>
|
||||
<h6 class="text-white mb-2">What MEMANTO does</h6>
|
||||
<ul class="text-secondary small mb-2 ps-3">
|
||||
<li>Your AI assistant <strong class="text-white">remembers context</strong> across sessions and browser refreshes</li>
|
||||
<li>Writing preferences <strong class="text-white">carry over</strong> between posts</li>
|
||||
<li>Eliminates <strong class="text-white">context loss</strong> when switching between Chat, Planning, and Writing modes</li>
|
||||
<li>Reduces AI token usage by up to <strong class="text-white">63%</strong> through smart memory recall</li>
|
||||
</ul>
|
||||
<p class="text-secondary small mb-0">
|
||||
Don't have MEMANTO? <a href="https://wpagentic.dev/memanto" target="_blank" class="text-info">Get MEMANTO Context Keeper</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
jQuery(document).ready(function($) {
|
||||
// Test MEMANTO connection.
|
||||
$('#wpaw-test-memanto').on('click', function() {
|
||||
var btn = $(this);
|
||||
var statusEl = $('#memanto-status-indicator');
|
||||
var detailEl = $('#memanto-status-detail');
|
||||
|
||||
btn.prop('disabled', true).html('<span class="spinner-border spinner-border-sm me-1"></span> Testing...');
|
||||
statusEl.removeClass('bg-success bg-danger bg-secondary bg-warning').addClass('bg-warning').html('Testing...');
|
||||
detailEl.text('');
|
||||
|
||||
$.post(wpawSettingsV2.ajaxUrl, {
|
||||
action: 'wpaw_test_memanto',
|
||||
nonce: wpawSettingsV2.nonce,
|
||||
url: $('#memanto_url').val(),
|
||||
key: $('#memanto_moorcheh_key').val()
|
||||
}, function(response) {
|
||||
btn.prop('disabled', false).html('<i class="bi bi-wifi me-1"></i> Test Connection');
|
||||
|
||||
if (response.success && response.data.healthy) {
|
||||
statusEl.removeClass('bg-warning bg-danger bg-secondary').addClass('bg-success').html('🟢 Connected');
|
||||
var detail = response.data.details || {};
|
||||
var info = detail.service ? detail.service + ' v' + (detail.version || '?') : 'Healthy';
|
||||
if (detail.moorcheh_connected) {
|
||||
info += ' · Moorcheh connected';
|
||||
}
|
||||
detailEl.text(info);
|
||||
} else {
|
||||
statusEl.removeClass('bg-warning bg-success bg-secondary').addClass('bg-danger').html('🔴 Error');
|
||||
detailEl.text(response.data?.message || 'Connection failed');
|
||||
}
|
||||
}).fail(function() {
|
||||
btn.prop('disabled', false).html('<i class="bi bi-wifi me-1"></i> Test Connection');
|
||||
statusEl.removeClass('bg-warning bg-success bg-secondary').addClass('bg-danger').html('🔴 Error');
|
||||
detailEl.text('Request failed');
|
||||
});
|
||||
});
|
||||
|
||||
// Auto-test on load if both fields are filled.
|
||||
if ($('#memanto_url').val() && $('#memanto_moorcheh_key').val()) {
|
||||
$('#wpaw-test-memanto').trigger('click');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
Reference in New Issue
Block a user