import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import {
advanceDecisionView,
countUnanswered,
cursor,
Decisions,
Face,
PlanEditor,
QuestionnaireStore,
ThreadStore,
useHasPlanContent,
useQuestionnaires,
visibleDecisionView,
} from "@chopin/editor";
import { Chat } from "./chat/chat";
import { decisionAttention, DecisionViewControl } from "./decision-view-control";
import { DocumentPicker } from "./document-picker";
import * as Api from "./api";
import { HostedApp, HostedFailure, HostedLoading, HostedLogin } from "./hosted";
import { RepositoryPicker } from "./repository-picker";
import { Wire } from "./wire";
import { useWorkspaceMode, useWorkspaceState, Workspace } from "./workspace";
import { presentWorkspace } from "./workspace-model";
import type { Session } from "@chopin/protocol";
import type { DecisionView, DecisionViewState } from "@chopin/editor";
import type { Status } from "./wire";
function Header(
{
canEdit,
members,
label,
repository,
room,
}: {
canEdit: boolean;
members: Session.Member[];
label: string;
repository: Api.Repository;
room: string;
},
) {
return (
);
}
export function RoomWorkspace(
{
agent = true,
canEdit = true,
handle,
label,
repository,
room,
}: {
agent?: boolean;
canEdit?: boolean;
handle: string;
label: string;
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 setDesktopConversationOpen = (open: boolean) => {
dispatch({ type: "set-desktop-conversation", open });
if (!open) dispatch({ type: "set-conversation", open: false });
};
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}
onDesktopConversationOpen={setDesktopConversationOpen}
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 ;
}