agnets done

This commit is contained in:
2026-08-28 11:02:02 +05:30
parent ab9adde6cd
commit eaf08e061d
58 changed files with 3506 additions and 4308 deletions

View File

@@ -1,5 +1,5 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { base44 } from '@/api/base44Client';
import { API_BASE_URL, base44 } from '@/api/base44Client';
import { generateJobDescription, screenCandidate, matchTalentForJob } from './krowAi';
import { recalcProfilePatch } from './krowScore';
import { logActivity } from './userTracking';
@@ -177,12 +177,141 @@ export function useUserActivity() {
});
}
/**
* What Owliver could usefully be asked on this page.
*
* The server answers, and the server is the only thing that decides: it filters
* the readings against the caller's role and — with nothing typed — ranks them
* against the organization's actual state in PostgreSQL. This hook requests and
* caches; it does not score, filter or reorder.
*
* Keyed on the page and the query together, so every distinct composer state
* has its own cache entry and typing back to something already asked is
* answered without a request. `placeholderData` keeps the previous answer on
* screen while the next one is in flight, which is what stops the chip row
* emptying and refilling between keystrokes.
*
* A failure resolves to no suggestions rather than to an error state. The panel
* still works with none — the composer is what the reader came for — and an
* error banner over a suggestion is a bigger interruption than the suggestion
* was worth.
*
* The untyped request is the one that reads the database, so it is invalidated
* whenever something the context counts has changed. See `owliverContextKey`.
*/
export const owliverContextKey = ['owliverSuggestions'];
/**
* Whether this backend serves the endpoint at all.
*
* A deployment lag, not a bug, and it has happened: the route exists in the Go
* source and is not yet on the host a given dev server proxies to, which
* answers `404` for it and `200` for everything else. Every keystroke would
* then be a request that cannot succeed, and no later keystroke can change the
* answer — so the first `404` closes the circuit for the rest of the page load.
*
* Only `404` closes it. A `401` means the session is not signed in, a `500`
* means the server had a bad moment, and an unreachable API means it is being
* restarted: all three are worth trying again, and treating them as "this
* backend does not have the route" would silence suggestions permanently over a
* temporary fault. Module-level rather than component state because the fact is
* about the backend, not about one panel.
*/
let routeUnavailable = false;
/**
* Say it once, out loud.
*
* The circuit breaker below stops the requests; it must not stop the reader
* finding out why the chip row is empty. A suggestion that quietly never
* arrives is indistinguishable from a page that has nothing to suggest, and
* that ambiguity is what makes this fault expensive — it looks like a frontend
* bug and it is a deployment lag.
*
* So the panel degrades to no suggestions, and the console says exactly which
* request failed and what would fix it. Nothing is substituted for the missing
* answer: there is no fallback list, and no locally-ranked catalogue standing
* in for the server's. An empty row is the honest rendering of "the backend
* could not tell us".
*/
function reportMissingRoute() {
if (routeUnavailable) return;
routeUnavailable = true;
console.warn(
`[krow] ${API_BASE_URL}/owliver/suggestions answered 404. The backend this ` +
'frontend is talking to does not register that route, so Owliver will show ' +
'no suggestion chips until it is deployed. This is a backend deployment ' +
'version mismatch, not a frontend fault — the route exists in the Go source ' +
'(go-api/internal/httpserver/owliver.go). Nothing is being substituted for ' +
'the missing answer.'
);
}
/**
* Ask the API what could usefully be asked here.
*
* The one place the request is made, so the circuit breaker above cannot be
* bypassed by a caller that reaches for `base44.owliver` directly. Resolves to
* an empty list on any failure: the panel works with no suggestions — the
* composer is what the reader came for — and an error banner over a suggestion
* is a bigger interruption than the suggestion was worth.
*/
/** @param {any} request */
export async function fetchOwliverSuggestions({ page, query = '' } = {}) {
if (!page || routeUnavailable) return [];
try {
return await base44.owliver.suggestions({ page, query });
} catch (error) {
if (error?.status === 404) reportMissingRoute();
return [];
}
}
/**
* What Owliver could usefully be asked on this page.
*
* The server answers, and the server is the only thing that decides: it filters
* the readings against the caller's role and — with nothing typed — ranks them
* against the organization's actual state in PostgreSQL. This hook requests and
* caches; it does not score, filter or reorder.
*
* Keyed on the page and the query together, so every distinct composer state
* has its own cache entry and typing back to something already asked is
* answered without a request. `placeholderData` keeps the previous answer on
* screen while the next one is in flight, which is what stops the chip row
* emptying and refilling between keystrokes.
*
* The untyped request is the one that reads the database, so it is invalidated
* whenever something the context counts has changed. See `owliverContextKey`.
*/
/** @param {any} request */
export function useOwliverSuggestions({ page, query = '', enabled = true } = {}) {
const typed = String(query || '').trim();
return useQuery({
queryKey: ['owliverSuggestions', page || null, typed],
queryFn: () => fetchOwliverSuggestions({ page, query: typed }),
enabled: Boolean(page) && enabled,
/* Long enough that a keystroke returned to is instant, short enough that a
position created in another tab is reflected on the next open. */
staleTime: 30_000,
placeholderData: (previous) => previous,
/* `fetchOwliverSuggestions` never rejects, so a retry would only repeat a
request that already resolved. Said explicitly so the default of one
retry does not read as a safety net that is doing something. */
retry: false,
});
}
export function useCreateJobPosting() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: /** @param {any} data */ (data) => base44.entities.JobPosting.create(data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['jobPostings'] });
/* The position count the server ranks suggestions against has changed, so
what Owliver offers next has to be asked again rather than read from a
cache written before the record existed. */
queryClient.invalidateQueries({ queryKey: owliverContextKey });
logActivity('create_position');
},
});
@@ -195,6 +324,7 @@ export function useUpdateJobPosting() {
onSuccess: (_data, variables) => {
queryClient.invalidateQueries({ queryKey: ['jobPostings'] });
queryClient.invalidateQueries({ queryKey: ['jobPosting', variables.id] });
queryClient.invalidateQueries({ queryKey: owliverContextKey });
},
});
}
@@ -205,6 +335,7 @@ export function useCreateApplication() {
mutationFn: /** @param {any} data */ (data) => base44.entities.JobApplication.create(data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['applications'] });
queryClient.invalidateQueries({ queryKey: owliverContextKey });
logActivity('apply_job');
},
});
@@ -216,6 +347,7 @@ export function useUpdateApplication() {
mutationFn: /** @param {any} vars */ ({ id, data }) => base44.entities.JobApplication.update(id, data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['applications'] });
queryClient.invalidateQueries({ queryKey: owliverContextKey });
},
});
}
@@ -238,6 +370,7 @@ export function useScreenCandidate() {
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['applications'] });
queryClient.invalidateQueries({ queryKey: owliverContextKey });
logActivity('screen_candidate');
},
});
@@ -268,6 +401,7 @@ export function useScreenAllCandidates() {
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['applications'] });
queryClient.invalidateQueries({ queryKey: owliverContextKey });
logActivity('screen_candidate');
},
});
@@ -326,6 +460,7 @@ export function useHireCandidate() {
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['applications'] });
queryClient.invalidateQueries({ queryKey: owliverContextKey });
queryClient.invalidateQueries({ queryKey: ['staff'] });
/* The `hire_candidate` entry is written inside the same transaction as the
hire, so there is nothing to log here — only something to refetch. A
@@ -358,6 +493,7 @@ export function useCreateInterview() {
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['interviews'] });
queryClient.invalidateQueries({ queryKey: ['applications'] });
queryClient.invalidateQueries({ queryKey: owliverContextKey });
logActivity('start_interview');
},
});
@@ -507,6 +643,7 @@ export function useAssignWorkers() {
looking at three different moments. */
queryClient.invalidateQueries({ queryKey: ['assignments'] });
queryClient.invalidateQueries({ queryKey: ['applications'] });
queryClient.invalidateQueries({ queryKey: owliverContextKey });
queryClient.invalidateQueries({ queryKey: ['jobPostings'] });
queryClient.invalidateQueries({ queryKey: ['userActivity'] });
},
@@ -549,6 +686,7 @@ export function useMarkInterviewReady() {
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['applications'] });
queryClient.invalidateQueries({ queryKey: owliverContextKey });
queryClient.invalidateQueries({ queryKey: ['interviews'] });
queryClient.invalidateQueries({ queryKey: ['userActivity'] });
},