diff --git a/features/attributes/shared/msw/data.ts b/features/attributes/shared/msw/data.ts index 6f58e6401..357d8e21f 100644 --- a/features/attributes/shared/msw/data.ts +++ b/features/attributes/shared/msw/data.ts @@ -298,10 +298,14 @@ export const MOCK_ATTRIBUTE_DEFINITIONS: MockAttributeDefinition[] = [ }, ] -// ── Default attribute values — returned for any entity ──────────────────────── -// Shape: Record as returned by GET /api/attribute-values +// ── Live attribute values store (draft + published layers) ──────────────────── -export const DEFAULT_MOCK_VALUES: Record = { +// Deep clone helper +function cloneValues(v: Record): Record { + return JSON.parse(JSON.stringify(v)) as Record; +} + +const INITIAL_VALUES: Record = { age: 27, languages: ['en', 'is', 'fr'], ethnicity: 'caucasian', @@ -317,6 +321,37 @@ export const DEFAULT_MOCK_VALUES: Record = { travel_willing: true, } +// publishedValues = the last-committed state (what "Revert" goes back to) +let publishedValues: Record = cloneValues(INITIAL_VALUES); +// currentValues = what the editor shows (includes unpublished extractions) +let currentValues: Record = cloneValues(INITIAL_VALUES); + +/** Returns the current live values (draft layer) */ +export function getCurrentValues(): Record { + return currentValues; +} + +/** Applies a patch of extracted attribute values to the draft layer */ +export function patchCurrentValues(patch: Record): void { + currentValues = { ...currentValues, ...patch }; +} + +/** Commits current values as the new published baseline */ +export function publishCurrentValues(): void { + publishedValues = cloneValues(currentValues); +} + +/** Reverts current values to the last published baseline */ +export function revertCurrentValues(): void { + currentValues = cloneValues(publishedValues); +} + +// ── Default attribute values — returned for any entity ──────────────────────── +// Shape: Record as returned by GET /api/attribute-values + +/** @deprecated Use getCurrentValues() for mutable access */ +export const DEFAULT_MOCK_VALUES: Record = INITIAL_VALUES; + // ── Draft types and store ────────────────────────────────────────────────────── export interface MockAttributeDraft { diff --git a/features/attributes/shared/msw/handlers.ts b/features/attributes/shared/msw/handlers.ts index 5f09312d8..208a1b9fe 100644 --- a/features/attributes/shared/msw/handlers.ts +++ b/features/attributes/shared/msw/handlers.ts @@ -1,6 +1,6 @@ import { http, HttpResponse, delay } from 'msw' -import { MOCK_ATTRIBUTE_DEFINITIONS, DEFAULT_MOCK_VALUES, draftStore, type MockAttributeDraft } from './data' +import { MOCK_ATTRIBUTE_DEFINITIONS, getCurrentValues, revertCurrentValues, publishCurrentValues, draftStore, type MockAttributeDraft } from './data' /** * Attributes API Mock Handlers @@ -43,7 +43,7 @@ export const attributesHandlers = [ ) } - return HttpResponse.json(DEFAULT_MOCK_VALUES) + return HttpResponse.json(getCurrentValues()) }), // Also handle the profile-routed path for attribute definitions @@ -158,6 +158,7 @@ export const attributesHandlers = [ const drafts = draftStore.get(body.profileId) ?? [] const count = drafts.length draftStore.set(body.profileId, []) + publishCurrentValues() return HttpResponse.json({ publishedCount: count }) }), @@ -171,7 +172,15 @@ export const attributesHandlers = [ const selected = drafts.filter((d) => body.codes.includes(d.code)) const remaining = drafts.filter((d) => !body.codes.includes(d.code)) draftStore.set(body.profileId, remaining) + publishCurrentValues() return HttpResponse.json({ publishedCount: selected.length }) }), + + // Revert current values to published baseline + http.post('*/api/attribute-values/revert', async () => { + await delay(60) + revertCurrentValues() + return new HttpResponse(null, { status: 204 }) + }), ] diff --git a/features/profile-assistant/plugin-profile-assistant/src/AssistantProvider.tsx b/features/profile-assistant/plugin-profile-assistant/src/AssistantProvider.tsx index 3fe07dc1d..5e2580083 100644 --- a/features/profile-assistant/plugin-profile-assistant/src/AssistantProvider.tsx +++ b/features/profile-assistant/plugin-profile-assistant/src/AssistantProvider.tsx @@ -20,6 +20,7 @@ export interface AssistantProviderProps { onNavigateToCategory?: (category: string) => void; onDraftsPublished?: () => void; onProfileCreated?: (slug: string) => void; + onAttributesExtracted?: (codes: string[]) => void; } export function AssistantProvider({ @@ -27,6 +28,7 @@ export function AssistantProvider({ onNavigateToCategory, onDraftsPublished, onProfileCreated, + onAttributesExtracted, }: AssistantProviderProps) { // Resolve API base URL from service registry, with fallback for dev environments // where the registry files may not be present (e.g. the profile showcase). @@ -71,13 +73,17 @@ export function AssistantProvider({ const sendMessage = useCallback( async (content: string) => { - await sessionHook.sendMessage(apiBaseUrl, content); + const extractedCodes = await sessionHook.sendMessage(apiBaseUrl, content); // Auto-refresh draft preview after every message so the Preview/Save buttons stay live if (sessionHook.state.sessionId) { await draftHook.fetchDraftPreview(apiBaseUrl, sessionHook.state.sessionId); } + // Notify parent so it can invalidate attribute-values queries for live editor update + if (extractedCodes.length > 0) { + onAttributesExtracted?.(extractedCodes); + } }, - [apiBaseUrl, sessionHook, draftHook], + [apiBaseUrl, sessionHook, draftHook, onAttributesExtracted], ); const fetchDraftPreview = useCallback(async () => { @@ -129,6 +135,7 @@ export function AssistantProvider({ onNavigateToCategory, onDraftsPublished, onProfileCreated, + onAttributesExtracted, popoverState, setPopoverState, unreadCount, diff --git a/features/profile-assistant/plugin-profile-assistant/src/hooks/use-assistant-session.ts b/features/profile-assistant/plugin-profile-assistant/src/hooks/use-assistant-session.ts index 663a4ec2b..0bda23f0a 100644 --- a/features/profile-assistant/plugin-profile-assistant/src/hooks/use-assistant-session.ts +++ b/features/profile-assistant/plugin-profile-assistant/src/hooks/use-assistant-session.ts @@ -16,7 +16,7 @@ export interface AssistantSessionHook { profileType?: string, pageContext?: string, ) => Promise; - sendMessage: (apiBaseUrl: string, content: string) => Promise; + sendMessage: (apiBaseUrl: string, content: string) => Promise; reset: () => void; } @@ -82,10 +82,10 @@ export function useAssistantSession(): AssistantSessionHook { ); const sendMessage = useCallback( - async (apiBaseUrl: string, content: string) => { + async (apiBaseUrl: string, content: string): Promise => { if (!state.sessionId) { setState((prev) => ({ ...prev, error: 'No active session' })); - return; + return []; } // Optimistically append user message @@ -132,6 +132,8 @@ export function useAssistantSession(): AssistantSessionHook { ], error: null, })); + + return assistantMessage.extractedAttributes.map((a) => a.code); } catch (err) { // Remove optimistic message on failure setState((prev) => ({ @@ -139,6 +141,7 @@ export function useAssistantSession(): AssistantSessionHook { messages: prev.messages.filter((m) => m.id !== tempUserMsg.id), error: err instanceof Error ? err.message : 'Failed to send message', })); + return []; } }, [state.sessionId], diff --git a/features/profile-assistant/plugin-profile-assistant/src/types.ts b/features/profile-assistant/plugin-profile-assistant/src/types.ts index 064f699d5..94b762a2f 100644 --- a/features/profile-assistant/plugin-profile-assistant/src/types.ts +++ b/features/profile-assistant/plugin-profile-assistant/src/types.ts @@ -85,6 +85,7 @@ export interface AssistantContextValue { onNavigateToCategory: ((category: string) => void) | undefined; onDraftsPublished: (() => void) | undefined; onProfileCreated: ((slug: string) => void) | undefined; + onAttributesExtracted: ((codes: string[]) => void) | undefined; /** Popover state */ popoverState: PopoverState; setPopoverState: (state: PopoverState) => void; diff --git a/features/profile-assistant/shared/msw/handlers.ts b/features/profile-assistant/shared/msw/handlers.ts index 01df60a99..a3c5db80a 100644 --- a/features/profile-assistant/shared/msw/handlers.ts +++ b/features/profile-assistant/shared/msw/handlers.ts @@ -22,6 +22,8 @@ import { simulateBrowseReply, } from './data' +import { patchCurrentValues, publishCurrentValues } from '../../../attributes/shared/msw/data' + // ── Helpers ──────────────────────────────────────────────────────────────────── let messageCounter = 0 @@ -183,6 +185,16 @@ export const profileAssistantHandlers = [ } session.messages.push(assistantMessage) + // Auto-apply extracted attributes to the live attribute values store + // so the profile editor immediately reflects the draft changes + if (reply.extracted.length > 0) { + const patch: Record = {}; + for (const attr of reply.extracted) { + patch[attr.code] = attr.value; + } + patchCurrentValues(patch); + } + session.updatedAt = new Date().toISOString() sessionStore.set(id, session) @@ -247,6 +259,7 @@ export const profileAssistantHandlers = [ return HttpResponse.json({ message: 'Session not found' }, { status: 404 }) } + publishCurrentValues() return HttpResponse.json({ publishedCount: MOCK_DRAFT_ITEMS.length }) }), @@ -263,6 +276,7 @@ export const profileAssistantHandlers = [ const body = (await request.json()) as { codes: string[] } const selectedItems = MOCK_DRAFT_ITEMS.filter((item) => body.codes.includes(item.code)) + publishCurrentValues() return HttpResponse.json({ publishedCount: selectedItems.length }) }), diff --git a/features/profile/frontend-showcase/src/App.tsx b/features/profile/frontend-showcase/src/App.tsx index 48c64a1d3..95d992ebf 100644 --- a/features/profile/frontend-showcase/src/App.tsx +++ b/features/profile/frontend-showcase/src/App.tsx @@ -1,5 +1,5 @@ import { BrowserRouter, Routes, Route, useNavigate } from '@lilith/ui-router'; -import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { QueryClient, QueryClientProvider, useQueryClient } from '@tanstack/react-query'; import { NavigationBar } from './components/NavigationBar'; import { ClientView } from './routes/BrowseView'; import { ManageView } from './routes/ManageView'; @@ -34,6 +34,7 @@ function ManageRoute() { */ function AppRoutes() { const navigate = useNavigate(); + const queryClient = useQueryClient(); const handleNavigateToCategory = (category: string) => { // Attempt to activate the given category in the attribute editor @@ -46,9 +47,13 @@ function AppRoutes() { return ( { + // Invalidate attribute values queries so the editor refetches the updated draft values + void queryClient.invalidateQueries(); + }} onDraftsPublished={() => { - // Profile data will be refreshed by the editor's own query invalidation - console.log('[AssistantProvider] Drafts published'); + // Invalidate all queries to reflect newly published values + void queryClient.invalidateQueries(); }} onProfileCreated={(slug) => { navigate(`/providers/${slug}/edit`);