update draft issue
This commit is contained in:
@@ -77,6 +77,23 @@ export function AssistantPanelProvider({ role, pathname, children }) {
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* A question the page handed to the panel, waiting to be asked.
|
||||
*
|
||||
* The one thing a page could not do before: publish what it is looking at
|
||||
* (PageContext) but never *start* a turn about it. So a control whose work
|
||||
* lives in the conversation — Continue, on a draft card — had no way to reach
|
||||
* the conversation, and the only thing left to do with it was navigate to a
|
||||
* form. This is that missing direction, kept as narrow as the read channel:
|
||||
* serializable state, no callbacks into the panel, and the panel decides when
|
||||
* to take it.
|
||||
*
|
||||
* State rather than an event, because the panel is unmounted while collapsed —
|
||||
* `ask` opens it, and the request is still here when it mounts.
|
||||
*/
|
||||
const [request, setRequest] = React.useState(null);
|
||||
const requestSeq = React.useRef(0);
|
||||
|
||||
const context = React.useMemo(
|
||||
() => resolveAssistantContext(role, pathname),
|
||||
[role, pathname]
|
||||
@@ -118,6 +135,27 @@ export function AssistantPanelProvider({ role, pathname, children }) {
|
||||
return () => window.removeEventListener('keydown', onKey);
|
||||
}, [isOpen, isExpanded, setExpandedState, setOpenState]);
|
||||
|
||||
/**
|
||||
* Asks Owliver something on the page's behalf, opening the panel to receive it.
|
||||
*
|
||||
* `positionId` names the record the control was built from, so the answer is
|
||||
* about that position rather than about whichever one the wording happens to
|
||||
* match. Each call carries its own id, so asking the same question twice is
|
||||
* two turns rather than one ignored repeat.
|
||||
*/
|
||||
const ask = React.useCallback(({ question, positionId = null }) => {
|
||||
const text = String(question || '').trim();
|
||||
if (!text) return;
|
||||
requestSeq.current += 1;
|
||||
setRequest({ id: requestSeq.current, question: text, positionId });
|
||||
setOpenState(true);
|
||||
}, [setOpenState]);
|
||||
|
||||
/** Taken by the panel once it has actually asked it. */
|
||||
const consumeRequest = React.useCallback((id) => {
|
||||
setRequest((current) => (current?.id === id ? null : current));
|
||||
}, []);
|
||||
|
||||
/* Width is written on every drag frame, so persistence is debounced rather
|
||||
than hitting sessionStorage sixty times a second. */
|
||||
const setWidthState = React.useCallback((next) => {
|
||||
@@ -148,7 +186,11 @@ export function AssistantPanelProvider({ role, pathname, children }) {
|
||||
toggle: () => setOpenState(!isOpen),
|
||||
expand: () => setExpandedState(true),
|
||||
restore: () => setExpandedState(false),
|
||||
}), [context, supported, isOpen, isExpanded, width, setWidthState, setOpenState, setExpandedState]);
|
||||
ask,
|
||||
request,
|
||||
consumeRequest,
|
||||
}), [context, supported, isOpen, isExpanded, width, setWidthState, setOpenState, setExpandedState,
|
||||
ask, request, consumeRequest]);
|
||||
|
||||
/* The page's own selection travels beside the window state, mounted here so
|
||||
both the page and the panel are inside it — a skill's data source resolves
|
||||
@@ -182,5 +224,8 @@ export function useAssistantPanel() {
|
||||
toggle: () => {},
|
||||
expand: () => {},
|
||||
restore: () => {},
|
||||
ask: () => {},
|
||||
request: null,
|
||||
consumeRequest: () => {},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -8,7 +8,8 @@ import { Surface } from '@/components/ds/Surface';
|
||||
import { IconButton } from '@/components/ds/IconButton';
|
||||
import { Alert } from '@/components/ds/Alert';
|
||||
import {
|
||||
useAssignments, useAssignWorkers, useCreateJobPosting, useMarkInterviewReady,
|
||||
useAssignments, useAssignWorkers, useCreateJobPosting, useGenerateJobDescription,
|
||||
useMarkInterviewReady, useUpdateJobPosting,
|
||||
usePreferences, useRoleCategories,
|
||||
} from '@/lib/krowHooks';
|
||||
import { ROLE_CATEGORIES } from '@/lib/roleCategories';
|
||||
@@ -19,6 +20,7 @@ import { owliverSuggestions } from '@/lib/skills/owliverResolver';
|
||||
import { useWorkforcePaths } from '@/lib/skills/usePageSkills';
|
||||
import { profileForEmail } from '@/lib/skillGraph';
|
||||
import { groupByRecency } from './history';
|
||||
import { useAssistantPanel } from './AssistantPanelContext';
|
||||
import { usePageContext } from './PageContext';
|
||||
import { useAssistantFacts, useConversation, useCurrentUserName } from './useAssistant';
|
||||
import { buildIntro, buildPrompts } from './dynamic';
|
||||
@@ -214,6 +216,9 @@ export default function KrowAssistant({
|
||||
const userName = useCurrentUserName();
|
||||
const navigate = useNavigate();
|
||||
|
||||
/* A question a page has handed over — see the effect below `runPrompt`. */
|
||||
const { request: panelRequest, consumeRequest } = useAssistantPanel();
|
||||
|
||||
/* The app's own router, not a location assignment: a full page load would
|
||||
discard the thread and the panel state along with it. */
|
||||
const goToPage = React.useCallback(
|
||||
@@ -320,6 +325,30 @@ export default function KrowAssistant({
|
||||
return createJob.mutateAsync(result.data);
|
||||
}, [createJob]);
|
||||
|
||||
/**
|
||||
* Finishing a draft, through the mutations the Create Position form calls.
|
||||
*
|
||||
* `useUpdateJobPosting` is the same update the form and the Positions page
|
||||
* use, and `useGenerateJobDescription` is the same generator behind the
|
||||
* form's "Generate Job Description with AI" button. Reusing both is what
|
||||
* keeps one position record, one description and one set of vetting weights
|
||||
* — the panel changes the record, it does not keep a copy of it.
|
||||
*/
|
||||
const updateJob = useUpdateJobPosting();
|
||||
const onUpdatePosition = React.useCallback(
|
||||
(id, data) => updateJob.mutateAsync({ id, data }),
|
||||
[updateJob]
|
||||
);
|
||||
|
||||
const generateDescription = useGenerateJobDescription();
|
||||
const onGenerateDescription = React.useCallback(
|
||||
async (position) => {
|
||||
const result = await generateDescription.mutateAsync({ data: position, draftId: position.id });
|
||||
return result || null;
|
||||
},
|
||||
[generateDescription]
|
||||
);
|
||||
|
||||
/**
|
||||
* The workforce picture the panel reasons over.
|
||||
*
|
||||
@@ -386,6 +415,8 @@ export default function KrowAssistant({
|
||||
onNavigate: goToPage,
|
||||
onAction: performAction,
|
||||
onCreatePosition: createPosition,
|
||||
onUpdatePosition,
|
||||
onGenerateDescription,
|
||||
onAssignWorkers,
|
||||
onScheduleInterview,
|
||||
workforce,
|
||||
@@ -495,14 +526,14 @@ export default function KrowAssistant({
|
||||
* bypasses intent routing. A typed question carries none and is routed. Both
|
||||
* land in the same `send`, so there is a single pipeline to reason about.
|
||||
*/
|
||||
const handleMessage = React.useCallback((question, capability = null) => {
|
||||
const handleMessage = React.useCallback((question, capability = null, positionId = null) => {
|
||||
const text = String(question).trim();
|
||||
if (!text || busy) return;
|
||||
setInput('');
|
||||
/* Asking something while reading History returns to the conversation — the
|
||||
answer is about to arrive there, and leaving the list up would hide it. */
|
||||
setView('chat');
|
||||
send({ question: text, capability });
|
||||
send({ question: text, capability, positionId });
|
||||
}, [busy, send]);
|
||||
|
||||
const submit = React.useCallback((text) => handleMessage(text), [handleMessage]);
|
||||
@@ -519,9 +550,26 @@ export default function KrowAssistant({
|
||||
navigate(prompt.route);
|
||||
return;
|
||||
}
|
||||
handleMessage(prompt.prompt, prompt.capability);
|
||||
/* A chip built from a position says which one, so a follow-up acts on the
|
||||
record the previous answer was about rather than re-deriving it from the
|
||||
words. */
|
||||
handleMessage(prompt.prompt, prompt.capability, prompt.positionId);
|
||||
}, [handleMessage, navigate]);
|
||||
|
||||
/**
|
||||
* A question the page asked on the reader's behalf.
|
||||
*
|
||||
* Continue on a draft card is the only source today: the card knows which
|
||||
* record it is, and the work of finishing it lives here. Deferred while a
|
||||
* response is streaming — the effect re-runs when the panel is free, so the
|
||||
* request waits its turn rather than being dropped for arriving mid-answer.
|
||||
*/
|
||||
React.useEffect(() => {
|
||||
if (!panelRequest || busy) return;
|
||||
handleMessage(panelRequest.question, null, panelRequest.positionId);
|
||||
consumeRequest(panelRequest.id);
|
||||
}, [panelRequest, busy, handleMessage, consumeRequest]);
|
||||
|
||||
const composer = (
|
||||
<PromptInput
|
||||
value={input}
|
||||
|
||||
@@ -12,6 +12,9 @@ import { pct, plural, verb } from '../insights';
|
||||
one thing in the library and another in the panel beside it. */
|
||||
import { TYPE_LABEL } from '@/components/forge/challengeMeta';
|
||||
import { toFICO } from '@/lib/talentHome';
|
||||
/* The vetting criteria's own labels. Read from the position model rather than
|
||||
restated here, so the panel calls a weight what the form calls it. */
|
||||
import { CRITERIA_LABELS } from '@/lib/positionModel';
|
||||
import { PERMISSIONS, PROFILE_ACTIONS } from '@/pages/admin/permissions';
|
||||
|
||||
/**
|
||||
|
||||
@@ -451,11 +451,18 @@ const PROMPTS = {
|
||||
*/
|
||||
const drafts = (f.postings || []).filter((p) => p.status === 'draft');
|
||||
if (drafts.length) {
|
||||
out.push({
|
||||
label: drafts.length === 1 ? `Continue the ${drafts[0].title} draft` : `${drafts.length} drafts to finish`,
|
||||
prompt: 'Which positions are still in draft?',
|
||||
capability: 'position-drafts',
|
||||
});
|
||||
out.push(drafts.length === 1
|
||||
/* Names the draft, so the answer continues *that* record in the panel
|
||||
rather than listing every draft and sending the reader to a form. */
|
||||
? {
|
||||
label: `Continue the ${drafts[0].title} draft`,
|
||||
prompt: `Continue the ${drafts[0].title} draft`,
|
||||
}
|
||||
: {
|
||||
label: `${drafts.length} drafts to finish`,
|
||||
prompt: 'Which positions are still in draft?',
|
||||
capability: 'position-drafts',
|
||||
});
|
||||
}
|
||||
|
||||
const current = workforce?.currentPositionId
|
||||
|
||||
@@ -7,6 +7,10 @@ import {
|
||||
matchOwliverSkill, owliverSkillsForContext, presentable, resolveOwliverResponse,
|
||||
responseTitle, summaryLines,
|
||||
} from '@/lib/skills/owliverResolver';
|
||||
import {
|
||||
draftActions, draftContinuedReply, explainWeightsReply, matchDraftIntent, noDraftReply,
|
||||
resolveDraft, weightsFromQuestion, weightsReply, weightsUnchangedReply, whichDraftReply,
|
||||
} from '@/lib/skills/draftFlow';
|
||||
import {
|
||||
appliedToday, askWhichPosition, assignmentPreview, assignmentToExecute, availability,
|
||||
candidateDetail, candidateMatches, candidateRoute, interviewReady, matchWorkforceIntent,
|
||||
@@ -707,12 +711,121 @@ function resolveSkill({
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* A draft question, resolved against the position records the panel holds.
|
||||
*
|
||||
* Returns an intent the panel executes. The two that write — generating a
|
||||
* description and setting weights, plus publishing — carry a `perform` block
|
||||
* rather than performing anything here: this module stays pure, and the write
|
||||
* goes through the mutation the Create Position form already uses.
|
||||
*/
|
||||
function resolveDraftAction(question, workforce, positionId = null) {
|
||||
const action = matchDraftIntent(question);
|
||||
if (!action) return null;
|
||||
|
||||
const positions = workforce.positions || [];
|
||||
/* `positionId` is the record a control was built from — the Continue button on
|
||||
a card, or a chip this flow wrote. It settles the question before any
|
||||
guessing from the wording starts. */
|
||||
const { position, ambiguous } = resolveDraft(
|
||||
question, positions, workforce.currentPositionId, positionId
|
||||
);
|
||||
|
||||
if (ambiguous) return { kind: 'draft', ...whichDraftReply(ambiguous) };
|
||||
|
||||
/**
|
||||
* Nothing resolved. Only a question explicitly about drafts is answered with
|
||||
* "there are none" — everything else is handed back.
|
||||
*
|
||||
* "Summarize these vetting weights" on the Create Position form is a question
|
||||
* about the form, and the page has answered it since long before drafts could
|
||||
* be continued here. Claiming it because it contains the word "weights" and
|
||||
* then reporting that no draft exists would be this flow taking a question it
|
||||
* cannot answer.
|
||||
*/
|
||||
if (!position) {
|
||||
return action === 'continue' || action === 'list'
|
||||
? { kind: 'draft', doc: noDraftReply() }
|
||||
: null;
|
||||
}
|
||||
|
||||
switch (action) {
|
||||
/* One draft is continued; several are offered by name. Either way the
|
||||
answer is here, and neither opens a form. */
|
||||
case 'list':
|
||||
return positions.filter((p) => p.status === 'draft').length > 1
|
||||
? { kind: 'draft', ...whichDraftReply(positions.filter((p) => p.status === 'draft')) }
|
||||
: {
|
||||
kind: 'draft',
|
||||
doc: draftContinuedReply(position),
|
||||
followUp: draftActions(position),
|
||||
};
|
||||
|
||||
case 'continue':
|
||||
return {
|
||||
kind: 'draft',
|
||||
doc: draftContinuedReply(position),
|
||||
/* Never "continue" again — the reply *is* the continuation. */
|
||||
followUp: draftActions(position),
|
||||
};
|
||||
|
||||
case 'show_weights':
|
||||
return { kind: 'draft', doc: weightsReply(position), followUp: draftActions(position) };
|
||||
|
||||
case 'explain_weights':
|
||||
return { kind: 'draft', doc: explainWeightsReply(position), followUp: draftActions(position) };
|
||||
|
||||
case 'set_weights': {
|
||||
const next = weightsFromQuestion(question, position);
|
||||
if (!next) {
|
||||
return { kind: 'draft', doc: weightsUnchangedReply(position), followUp: draftActions(position) };
|
||||
}
|
||||
return { kind: 'draft', perform: { action: 'set_weights', position, weights: next } };
|
||||
}
|
||||
|
||||
case 'generate_description':
|
||||
return { kind: 'draft', perform: { action: 'generate_description', position } };
|
||||
|
||||
case 'publish':
|
||||
return position.status === 'draft'
|
||||
? { kind: 'draft', perform: { action: 'publish', position } }
|
||||
: {
|
||||
kind: 'draft',
|
||||
doc: doc(text(`**${position.title}** is already published.`)),
|
||||
followUp: draftActions(position),
|
||||
};
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveIntent({
|
||||
question, contextId, disabledSkills = [], customSkills = [], roles = [], skillCategories = [],
|
||||
courses = [], workforce = null, skillContext = null,
|
||||
/* The record the control that raised this question was built from, when there
|
||||
was one. Only the draft flow reads it; a typed question carries none and
|
||||
resolves exactly as it always did. */
|
||||
positionId = null,
|
||||
}) {
|
||||
const context = ASSISTANT_CONTEXTS[contextId] ?? null;
|
||||
|
||||
/**
|
||||
* 1. Finishing a draft, wherever the reader is standing.
|
||||
*
|
||||
* Ahead of everything else because these questions name a record and an act
|
||||
* on it — continuing, generating, weighting, publishing — and every other
|
||||
* matcher would read them as words about positions in general. It runs only
|
||||
* where the panel actually has the positions to act on.
|
||||
*/
|
||||
/* Resolved even where the panel carries no workforce: a question about
|
||||
continuing a draft is answered in the panel or answered as "there is no
|
||||
draft", never handed to keyword routing — which is what used to send
|
||||
"Continue the Bartender draft" into the authoring form, because Create
|
||||
Position lists `position` and `role` among its topics. */
|
||||
const draftIntent = resolveDraftAction(question, workforce || { positions: [] }, positionId);
|
||||
if (draftIntent) return draftIntent;
|
||||
|
||||
/* 2. Current page skills — specific triggers, ahead of the general reader. */
|
||||
const skill = resolveSkill({
|
||||
question, contextId, disabledSkills, customSkills, roles, skillCategories, courses,
|
||||
|
||||
@@ -10,6 +10,10 @@ import { suggestionsForPosition } from '@/lib/skills/owliverResolver';
|
||||
import {
|
||||
advancePositionFlow, createdFollowUp, positionCreatedReply, positionFailedReply,
|
||||
} from '@/lib/skills/positionFlow';
|
||||
import {
|
||||
descriptionFailedReply, descriptionReply, draftActions, publishFailedReply, publishedFollowUp,
|
||||
publishedReply, weightsSetReply, weightsUnchangedReply,
|
||||
} from '@/lib/skills/draftFlow';
|
||||
import {
|
||||
assignmentDone, assignmentFailed, assignmentFollowUp, interviewDone, interviewFailed,
|
||||
} from '@/lib/skills/workforceFlow';
|
||||
@@ -118,6 +122,58 @@ async function streamDocument({ document, signal, onFrame, onDone }) {
|
||||
*/
|
||||
const AUTHORING_ROUTE = '/admin/positions/new';
|
||||
|
||||
/**
|
||||
* The one wording every draft-continuation control resolves to.
|
||||
*
|
||||
* Named once because two things have to agree about it: the chip a suggestion
|
||||
* writes today, and the chip a thread wrote months ago. Both must reach the
|
||||
* same in-panel handler, so both are normalized to this phrasing rather than
|
||||
* one being special-cased.
|
||||
*/
|
||||
const CONTINUE_DRAFT = /^continue the (.+) draft$/i;
|
||||
|
||||
/** The wording older builds used for the drafts question. */
|
||||
const OLD_DRAFTS_PROMPT = /which positions are still in draft/i;
|
||||
|
||||
/**
|
||||
* A stored chip, brought up to the current draft behavior.
|
||||
*
|
||||
* Threads persist as the blocks and chips they rendered, so a conversation
|
||||
* written before draft continuation moved into the panel still carries the
|
||||
* controls of that era: a `capability` that runs the read-only drafts report,
|
||||
* or a prompt phrased so loosely that keyword routing sent it to the authoring
|
||||
* form. Rewriting them in code was not enough — the ones already written down
|
||||
* had to change behavior too.
|
||||
*
|
||||
* Deliberately narrow. Only chips that are *about continuing a draft* are
|
||||
* touched, and only their routing metadata: the label the reader saw is left
|
||||
* exactly as it was, so a thread reads as it always did and simply does the
|
||||
* right thing when clicked.
|
||||
*/
|
||||
function normalizeDraftChip(chip) {
|
||||
if (!chip) return chip;
|
||||
|
||||
const label = String(chip.label || '');
|
||||
const prompt = String(chip.prompt || '');
|
||||
|
||||
/* "Continue the Bartender draft", however it was stored: the label is the
|
||||
question, and no capability or route may intercept it. */
|
||||
if (CONTINUE_DRAFT.test(label) || CONTINUE_DRAFT.test(prompt)) {
|
||||
const { capability, route, ...rest } = chip;
|
||||
return { ...rest, prompt: CONTINUE_DRAFT.test(label) ? label : prompt };
|
||||
}
|
||||
|
||||
/* The old drafts question ran a capability that could only describe drafts
|
||||
and told the reader to open the form. The same words now resolve to the
|
||||
draft flow, which continues one or offers the several by name. */
|
||||
if (OLD_DRAFTS_PROMPT.test(prompt) || chip.capability === 'position-drafts') {
|
||||
const { capability, route, ...rest } = chip;
|
||||
return { ...rest, prompt: prompt || 'Which positions are still in draft?' };
|
||||
}
|
||||
|
||||
return chip;
|
||||
}
|
||||
|
||||
/**
|
||||
* A stored thread, with any completed-then-reopen action stripped out.
|
||||
*
|
||||
@@ -138,7 +194,9 @@ function withoutAuthoringActions(messages = []) {
|
||||
return messages.map((message) => {
|
||||
if (message.role !== 'assistant') return message;
|
||||
|
||||
const followUp = message.followUp?.filter((chip) => !leadsToForm(chip.route));
|
||||
const followUp = message.followUp
|
||||
?.filter((chip) => !leadsToForm(chip.route))
|
||||
.map(normalizeDraftChip);
|
||||
const blocks = message.blocks?.map((block) => (
|
||||
block?.type === 'insights'
|
||||
? {
|
||||
@@ -168,6 +226,10 @@ function withoutAuthoringActions(messages = []) {
|
||||
*/
|
||||
export function useConversation({
|
||||
contextId, facts, onNavigate, onAction, onCreatePosition, onAssignWorkers, onScheduleInterview,
|
||||
/* Finishing a draft: the same two mutations the Create Position form calls.
|
||||
Passed in rather than reached for, so this layer still writes nothing
|
||||
itself and there is one update path for a position. */
|
||||
onUpdatePosition, onGenerateDescription,
|
||||
workforce = null, disabledSkills = [], customSkills = [],
|
||||
roles = [], skillCategories = [], courses = [], skillContext = null,
|
||||
/* The page's own name, recorded with an archived thread so History can say
|
||||
@@ -281,7 +343,7 @@ export function useConversation({
|
||||
const messagesRef = React.useRef(messages);
|
||||
messagesRef.current = messages;
|
||||
|
||||
const send = React.useCallback(async ({ question, capability = null }) => {
|
||||
const send = React.useCallback(async ({ question, capability = null, positionId = null }) => {
|
||||
const text = String(question).trim();
|
||||
if (!text) return;
|
||||
|
||||
@@ -323,7 +385,7 @@ export function useConversation({
|
||||
? { kind: 'answer' }
|
||||
: resolveIntent({
|
||||
question: text, contextId, disabledSkills, customSkills, roles, skillCategories,
|
||||
courses, workforce, skillContext,
|
||||
courses, workforce, skillContext, positionId,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -377,6 +439,59 @@ export function useConversation({
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finishing a draft. Same contract as every other write in this file: it
|
||||
* happens before the reply, because the reply states the outcome.
|
||||
*
|
||||
* Each branch calls the mutation the form already uses and then says what
|
||||
* is now true of the record — no second store, and no navigation.
|
||||
*/
|
||||
if (intent.kind === 'draft' && intent.perform) {
|
||||
const { action, position } = intent.perform;
|
||||
|
||||
if (action === 'generate_description') {
|
||||
let result = null;
|
||||
try {
|
||||
result = await onGenerateDescription?.(position);
|
||||
} catch {
|
||||
result = null;
|
||||
}
|
||||
intent = {
|
||||
...intent,
|
||||
doc: result?.description ? descriptionReply(position, result) : descriptionFailedReply(position),
|
||||
followUp: draftActions(result?.description ? { ...position, description: result.description } : position),
|
||||
};
|
||||
}
|
||||
|
||||
if (action === 'set_weights') {
|
||||
let saved = null;
|
||||
try {
|
||||
saved = await onUpdatePosition?.(position.id, { vetting_criteria: intent.perform.weights });
|
||||
} catch {
|
||||
saved = null;
|
||||
}
|
||||
intent = {
|
||||
...intent,
|
||||
doc: saved ? weightsSetReply(position, intent.perform.weights) : weightsUnchangedReply(position),
|
||||
followUp: draftActions(saved || position),
|
||||
};
|
||||
}
|
||||
|
||||
if (action === 'publish') {
|
||||
let published = null;
|
||||
try {
|
||||
published = await onUpdatePosition?.(position.id, { status: 'active' });
|
||||
} catch {
|
||||
published = null;
|
||||
}
|
||||
intent = {
|
||||
...intent,
|
||||
doc: published ? publishedReply(position) : publishFailedReply(position),
|
||||
followUp: published ? publishedFollowUp(published) : draftActions(position),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The workforce write, on the same terms as the one above: it happens
|
||||
* before the reply, because the reply states the new counts and those have
|
||||
@@ -481,13 +596,19 @@ export function useConversation({
|
||||
}
|
||||
} catch (e) {
|
||||
if (e?.name !== 'AbortError') {
|
||||
/* The reader gets one sentence; the developer gets the exception. A
|
||||
capability that throws used to leave no trace anywhere, so "I could
|
||||
not complete that" was the only evidence anything had gone wrong —
|
||||
and it named neither the action nor the reason. */
|
||||
if (import.meta.env.DEV) console.error('[owliver] action failed', e);
|
||||
setError('I could not complete that. Try again in a moment.');
|
||||
}
|
||||
} finally {
|
||||
setPending(null);
|
||||
abortRef.current = null;
|
||||
}
|
||||
}, [contextId, facts, persist, onNavigate, onAction, onCreatePosition, onAssignWorkers,
|
||||
}, [contextId, facts, persist, onNavigate, onAction, onCreatePosition, onUpdatePosition,
|
||||
onGenerateDescription, onAssignWorkers,
|
||||
onScheduleInterview,
|
||||
workforce, setFlow, disabledSkills,
|
||||
customSkills, roles, skillCategories, courses, skillContext]);
|
||||
|
||||
Reference in New Issue
Block a user