;
label: string;
repository: Api.Repository;
showConversationToggle: boolean;
},
) {
let [resetting, setResetting] = useState(false);
let [resetError, setResetError] = useState(false);
let resetLabel = resetting ? "Resetting..." : resetError ? "Reset failed" : "New planner session";
async function resetAgent() {
if (!onResetAgent || resetting) return;
setResetting(true);
setResetError(false);
try {
await onResetAgent();
} catch {
setResetError(true);
} finally {
setResetting(false);
}
}
return (
);
}
export function RoomWorkspace(
{
agent = true,
canEdit = true,
handle,
label,
onResetAgent,
repository,
room,
}: {
agent?: boolean;
canEdit?: boolean;
handle: string;
label: string;
onResetAgent?: () => Promise;
repository: Api.Repository;
room: string;
},
) {
let [wire, setWire] = useState();
let [status, setStatus] = useState("connecting");
let [members, setMembers] = useState([]);
let [effectiveCanEdit, setEffectiveCanEdit] = useState(canEdit);
let user = useMemo(() => cursor(handle), [handle]);
let mode = useWorkspaceMode();
let [workspace, dispatch] = useWorkspaceState();
let [questions] = useState(() => new QuestionnaireStore());
let [threads] = useState(() => new ThreadStore());
let [reveal, setReveal] = useState<{ widget: string; token: number }>();
let [planScrollTop, setPlanScrollTop] = useState(0);
let entries = useQuestionnaires(questions);
let unanswered = countUnanswered(entries);
let hasPlanContent = useHasPlanContent(questions);
let [decisionView, setDecisionView] = useState(() => {
let stored = localStorage.getItem("chopin:view:document");
return {
phase: "initial",
preferred: stored === "decisions" ? "decisions" : "plan",
};
});
let view = visibleDecisionView(decisionView, hasPlanContent, unanswered);
let previousUnanswered = useRef(unanswered);
let [attention, setAttention] = useState(false);
let workspacePresentation = presentWorkspace(workspace, mode, view);
let conversationActive = workspacePresentation.conversationVisible;
let [conversationActivity, setConversationActivity] = useState({ unread: 0, busy: false });
let onConversationActivity = useCallback(
(event: { type: "message" | "working"; busy: boolean }) => {
setConversationActivity(current => ({
busy: event.busy,
unread: event.type === "message" && !conversationActive
? current.unread + 1
: current.unread,
}));
},
[conversationActive],
);
useEffect(() => {
setDecisionView(state => advanceDecisionView(state, hasPlanContent, unanswered));
}, [hasPlanContent, unanswered]);
useEffect(() => {
if (!conversationActive) return;
setConversationActivity(current => current.unread === 0 ? current : { ...current, unread: 0 });
}, [conversationActive]);
useEffect(() => {
let previous = previousUnanswered.current;
previousUnanswered.current = unanswered;
if (!decisionAttention(previous, unanswered)) return;
setAttention(true);
let timer = window.setTimeout(() => setAttention(false), 200);
return () => window.clearTimeout(timer);
}, [unanswered]);
let selectView = (next: DecisionView, revealFirst = true) => {
setDecisionView(state => ({ ...state, preferred: next }));
if (hasPlanContent) localStorage.setItem("chopin:view:document", next);
if (next === "decisions" && revealFirst) {
let first = entries.find(entry =>
entry.value.questions.some(question => question.answer === undefined)
);
setReveal({ widget: first?.id ?? "", token: Date.now() });
}
};
let selectDestination = (destination: "plan" | "decisions") => {
selectView(destination, mode === "split");
dispatch({ type: "set-conversation", open: false });
};
let toggleConversation = () => {
if (mode !== "split") {
dispatch({ type: "set-conversation", open: !workspace.conversationOpen });
return;
}
if (workspacePresentation.conversationVisible) {
dispatch({ type: "set-desktop-conversation", open: false });
dispatch({ type: "set-conversation", open: false });
} else dispatch({ type: "set-desktop-conversation", open: true });
};
let showPlan = (widget: string, question: string) => {
selectDestination("plan");
requestAnimationFrame(() => {
questions.reveal(widget, question);
let card = document.querySelector(
`[data-document-view="plan"] article[data-plan-sidecar-questionnaire="${
CSS.escape(widget)
}"]`,
);
if (!card) return;
card.tabIndex = -1;
card.focus();
});
};
useEffect(() => {
setEffectiveCanEdit(canEdit);
}, [canEdit, room]);
useEffect(() => {
let socket = new Wire({
channelId: room,
onAuthenticationRequired: () => location.assign("/"),
onStatus: next => {
setStatus(next);
},
});
setWire(socket);
let off = [
socket.on("session:hello", frame => {
setMembers(frame.members);
setEffectiveCanEdit(frame.canEdit);
}),
socket.on("session:presence", frame => setMembers(frame.members)),
socket.on("session:access", frame => setEffectiveCanEdit(frame.canEdit)),
threads.listen(socket),
];
return () => {
for (let unsubscribe of off) unsubscribe();
socket.dispose();
setWire(undefined);
};
}, [room, handle, threads]);
return (
}
conversationActivity={conversationActivity}
header={
}
controls={
}
mode={mode}
onConversationOpen={open => dispatch({ type: "set-conversation", open })}
onDestination={selectDestination}
decisions={
}
plan={
}
state={workspace}
unanswered={unanswered}
view={view}
/>
);
}
export function App() {
let [session, setSession] = useState();
let [error, setError] = useState();
useEffect(() => {
let active = true;
Api.session().then(value => {
if (active) setSession(value);
}, reason => {
if (active) setError(reason);
});
return () => {
active = false;
};
}, []);
if (error) return ;
if (!session) return ;
if (!session.user) return ;
return ;
}