Files
zopu-code/apps/web/src/hooks/use-mobile-project-workspace.ts
T

127 lines
4.2 KiB
TypeScript

import { api } from "@code/backend/convex/_generated/api";
import type { Id } from "@code/backend/convex/_generated/dataModel";
import type {
MobileAssistantMessageView,
MobileAssistantView,
} from "@code/ui/components/mobile-product";
import { useQuery } from "convex/react";
import { useOrganizationChatAgent } from "@/hooks/chat/use-chat-agent";
import { usePersonalOrganization } from "@/hooks/use-personal-organization";
import { useProjectWorkspace } from "@/hooks/use-project-workspace";
import { STATUS_COPY } from "@/lib/chat/constants";
import { getMessageText } from "@/lib/chat/transforms";
import { buildMobileWorkspaceView } from "@/lib/mobile-workspace/build-mobile-workspace-view";
const WORK_UNIT_SCOPE_PATTERN =
/^<zopu-work-unit id="(?<workUnitId>[^"]+)">[\s\S]*?<message>(?<message>[\s\S]*?)<\/message>\s*<\/zopu-work-unit>$/u;
const toAssistantMessages = (
messages: ReturnType<typeof useOrganizationChatAgent>["messages"],
selectedWorkUnitId?: string
): MobileAssistantMessageView[] => {
const result: MobileAssistantMessageView[] = [];
for (const message of messages) {
if (message.role !== "assistant" && message.role !== "user") {
continue;
}
const rawText = getMessageText(message);
if (!rawText) {
continue;
}
if (!selectedWorkUnitId) {
result.push({ id: message.id, role: message.role, text: rawText });
continue;
}
if (message.role === "user") {
const match = WORK_UNIT_SCOPE_PATTERN.exec(rawText);
if (match?.groups?.workUnitId !== selectedWorkUnitId) {
result.length = 0;
continue;
}
result.push({
id: message.id,
role: message.role,
text: match.groups.message?.trim() ?? rawText,
});
continue;
}
if (result.length > 0) {
result.push({ id: message.id, role: message.role, text: rawText });
}
}
return result;
};
const getStatusTone = (
status: ReturnType<typeof useOrganizationChatAgent>["status"]
): MobileAssistantView["statusTone"] => {
if (status === "error") {
return "red";
}
return status === "connecting" ? "amber" : "green";
};
interface UseMobileProjectWorkspaceOptions {
readonly selectedWorkUnitId?: string;
}
export const useMobileProjectWorkspace = ({
selectedWorkUnitId,
}: UseMobileProjectWorkspaceOptions) => {
const organization = usePersonalOrganization();
const projectWorkspace = useProjectWorkspace();
const chatAgent = useOrganizationChatAgent(organization);
const signals = useQuery(
api.signals.list,
organization.organizationId
? { organizationId: organization.organizationId }
: "skip"
);
const selectedIssue = projectWorkspace.issues?.find(
(issue) => String(issue._id) === selectedWorkUnitId
);
const sendMessage = (message: string) => {
if (!selectedIssue) {
return chatAgent.sendMessage(message);
}
return chatAgent.sendMessage(
`<zopu-work-unit id="${selectedIssue._id}">\n<context>Issue #${selectedIssue.number}: ${selectedIssue.title}</context>\n<message>${message}</message>\n</zopu-work-unit>`
);
};
const assistant: MobileAssistantView = {
isBusy:
chatAgent.status === "connecting" ||
chatAgent.status === "streaming" ||
chatAgent.status === "submitted",
messages: toAssistantMessages(chatAgent.messages, selectedWorkUnitId),
statusLabel: STATUS_COPY[chatAgent.status],
statusTone: getStatusTone(chatAgent.status),
};
return {
data: buildMobileWorkspaceView({
artifacts: projectWorkspace.artifacts,
assistant,
events: projectWorkspace.events,
issues: projectWorkspace.issues,
organizationLabel: organization.organizationName,
projects: projectWorkspace.projects,
selectedProject: projectWorkspace.selectedProject,
selectedWorkUnitId,
signals,
}),
error:
organization.error ??
chatAgent.error ??
(projectWorkspace.error ? new Error(projectWorkspace.error) : undefined),
organization,
projectWorkspace,
raiseIssue: projectWorkspace.raiseIssue,
raiseIssueFromSignal: (signalId: string) =>
projectWorkspace.raiseIssueFromSignal(signalId as Id<"signals">),
sendMessage,
startWorkUnit: projectWorkspace.startWorkUnit,
} as const;
};