update position page design

This commit is contained in:
2026-08-13 21:41:14 +05:30
parent 6340962907
commit 1c653264b5
124 changed files with 5746 additions and 596 deletions

View File

@@ -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(), []);