update position page design
This commit is contained in:
@@ -9,6 +9,9 @@ import { skillsForContext } from '@/lib/skills/registry';
|
||||
import {
|
||||
advancePositionFlow, createdFollowUp, positionCreatedReply, positionFailedReply,
|
||||
} from '@/lib/skills/positionFlow';
|
||||
import {
|
||||
assignmentDone, assignmentFailed, assignmentFollowUp, interviewDone, interviewFailed,
|
||||
} from '@/lib/skills/workforceFlow';
|
||||
import { buildFacts } from './insights';
|
||||
import { createAssistantProvider } from './provider';
|
||||
import { resolveIntent } from './routing';
|
||||
@@ -74,14 +77,33 @@ export function useCurrentUserName() {
|
||||
*/
|
||||
async function streamDocument({ document, signal, onFrame, onDone }) {
|
||||
const snapshots = toSnapshots(document);
|
||||
const complete = snapshots[snapshots.length - 1] || [];
|
||||
|
||||
let latest = [];
|
||||
let interrupted = false;
|
||||
|
||||
for (const snapshot of snapshots) {
|
||||
if (signal?.aborted) break;
|
||||
if (signal?.aborted) { interrupted = true; break; }
|
||||
latest = snapshot;
|
||||
onFrame(snapshot);
|
||||
await new Promise((resolve) => setTimeout(resolve, 26));
|
||||
}
|
||||
if (latest.length) onDone(latest);
|
||||
|
||||
/**
|
||||
* An interrupted local document still settles complete.
|
||||
*
|
||||
* These documents are already fully computed before the first frame — the
|
||||
* reveal is presentation, not generation. So an interruption should not leave
|
||||
* a half-written sentence in the thread the way stopping a real generation
|
||||
* would: there is nothing partial about the answer, only about the animation.
|
||||
*
|
||||
* This matters most after a write. The mutation refreshes every query it
|
||||
* touched, the panel re-renders on the new data, and the reveal can be cut
|
||||
* short — leaving the confirmation of something that definitely happened
|
||||
* truncated mid-word.
|
||||
*/
|
||||
if (interrupted) onDone(complete);
|
||||
else if (latest.length) onDone(latest);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -97,7 +119,8 @@ async function streamDocument({ document, signal, onFrame, onDone }) {
|
||||
* routing applies to both without either knowing it exists.
|
||||
*/
|
||||
export function useConversation({
|
||||
contextId, facts, onNavigate, onAction, onCreatePosition, disabledSkills = [], customSkills = [],
|
||||
contextId, facts, onNavigate, onAction, onCreatePosition, onAssignWorkers, onScheduleInterview,
|
||||
workforce = null, disabledSkills = [], customSkills = [],
|
||||
roles = [], skillCategories = [], courses = [],
|
||||
}) {
|
||||
const storageKey = `krow_assistant:${contextId}`;
|
||||
@@ -200,7 +223,8 @@ export function useConversation({
|
||||
intent = capability
|
||||
? { kind: 'answer' }
|
||||
: resolveIntent({
|
||||
question: text, contextId, disabledSkills, customSkills, roles, skillCategories, courses,
|
||||
question: text, contextId, disabledSkills, customSkills, roles, skillCategories,
|
||||
courses, workforce,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -235,6 +259,51 @@ export function useConversation({
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* to be true when they are read.
|
||||
*
|
||||
* `intent.assign` only exists on a turn the admin explicitly confirmed, and
|
||||
* the plan inside it was re-derived from live data at that moment — so this
|
||||
* writes what was agreed to or reports that it could not.
|
||||
*/
|
||||
if (intent.kind === 'workforce' && intent.assign) {
|
||||
let done = null;
|
||||
try {
|
||||
done = await onAssignWorkers?.(intent.assign);
|
||||
} catch {
|
||||
done = null;
|
||||
}
|
||||
|
||||
intent = {
|
||||
...intent,
|
||||
kind: 'answer-doc',
|
||||
doc: done ? assignmentDone(intent.assign) : assignmentFailed(),
|
||||
followUp: done ? assignmentFollowUp(intent.assign) : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* The interview step. Same contract as the assignment above: it runs before
|
||||
* the reply, only on a confirmed turn, and through the app's existing
|
||||
* status transition rather than a second interview store.
|
||||
*/
|
||||
if (intent.kind === 'workforce' && intent.interview) {
|
||||
let moved = null;
|
||||
try {
|
||||
moved = await onScheduleInterview?.(intent.interview);
|
||||
} catch {
|
||||
moved = null;
|
||||
}
|
||||
|
||||
intent = {
|
||||
...intent,
|
||||
kind: 'answer-doc',
|
||||
doc: moved ? interviewDone(intent.interview) : interviewFailed(),
|
||||
};
|
||||
}
|
||||
|
||||
/* A skill that collects its input in the chat carries the state of that
|
||||
collection on every turn — including the turn that starts it. */
|
||||
if ('flow' in intent) setFlow(intent.flow);
|
||||
@@ -300,7 +369,9 @@ export function useConversation({
|
||||
setPending(null);
|
||||
abortRef.current = null;
|
||||
}
|
||||
}, [contextId, facts, persist, onNavigate, onAction, onCreatePosition, setFlow, disabledSkills,
|
||||
}, [contextId, facts, persist, onNavigate, onAction, onCreatePosition, onAssignWorkers,
|
||||
onScheduleInterview,
|
||||
workforce, setFlow, disabledSkills,
|
||||
customSkills, roles, skillCategories, courses]);
|
||||
|
||||
const stop = React.useCallback(() => abortRef.current?.abort(), []);
|
||||
|
||||
Reference in New Issue
Block a user