mirror of
https://github.com/aiclientproxy/proxycast.git
synced 2026-10-24 23:31:47 +08:00
639 lines
17 KiB
TypeScript
639 lines
17 KiB
TypeScript
import {
|
|
useCallback,
|
|
useEffect,
|
|
useMemo,
|
|
useReducer,
|
|
useState,
|
|
type Dispatch,
|
|
type SetStateAction,
|
|
} from "react";
|
|
import { toast } from "sonner";
|
|
import {
|
|
createContent,
|
|
createProject,
|
|
extractErrorMessage,
|
|
getContent,
|
|
getContentTypeLabel,
|
|
getCreateProjectErrorMessage,
|
|
getDefaultContentTypeForProject,
|
|
getProjectByRootPath,
|
|
getProjectTypeLabel,
|
|
getWorkspaceProjectsRoot,
|
|
resolveProjectRootPath,
|
|
type ProjectType,
|
|
} from "@/lib/api/project";
|
|
import type { WorkspaceTheme } from "@/types/page";
|
|
import type { CreationMode } from "@/components/content-creator/types";
|
|
import {
|
|
buildCreationIntentMetadata,
|
|
buildCreationIntentPrompt,
|
|
createInitialCreationIntentValues,
|
|
getCreationIntentFieldsSafe,
|
|
isCreationMode,
|
|
normalizeCreationMode,
|
|
type CreationIntentFieldKey,
|
|
type CreationIntentFormValues,
|
|
type CreationIntentInput,
|
|
validateCreationIntent,
|
|
} from "@/components/workspace/utils/creationIntentPrompt";
|
|
import { reportFrontendError } from "@/lib/crashReporting";
|
|
|
|
type CreateContentDialogStep = "mode" | "intent";
|
|
|
|
interface CreateContentDialogState {
|
|
step: CreateContentDialogStep;
|
|
selectedCreationMode: CreationMode;
|
|
creationIntentValues: CreationIntentFormValues;
|
|
creationIntentError: string;
|
|
}
|
|
|
|
type CreateContentDialogAction =
|
|
| {
|
|
type: "reset";
|
|
defaultMode: CreationMode;
|
|
}
|
|
| {
|
|
type: "setStep";
|
|
step: CreateContentDialogStep;
|
|
}
|
|
| {
|
|
type: "setMode";
|
|
mode: unknown;
|
|
}
|
|
| {
|
|
type: "setError";
|
|
error: string;
|
|
}
|
|
| {
|
|
type: "updateIntentValue";
|
|
key: CreationIntentFieldKey;
|
|
value: string;
|
|
}
|
|
| {
|
|
type: "goIntentStep";
|
|
};
|
|
|
|
function createInitialContentDialogState(
|
|
defaultMode: CreationMode,
|
|
): CreateContentDialogState {
|
|
return {
|
|
step: "mode",
|
|
selectedCreationMode: normalizeCreationMode(defaultMode),
|
|
creationIntentValues: createInitialCreationIntentValues(),
|
|
creationIntentError: "",
|
|
};
|
|
}
|
|
|
|
function createContentDialogReducer(
|
|
state: CreateContentDialogState,
|
|
action: CreateContentDialogAction,
|
|
): CreateContentDialogState {
|
|
switch (action.type) {
|
|
case "reset":
|
|
return createInitialContentDialogState(action.defaultMode);
|
|
case "setStep":
|
|
return {
|
|
...state,
|
|
step: action.step,
|
|
};
|
|
case "setMode":
|
|
return {
|
|
...state,
|
|
selectedCreationMode: normalizeCreationMode(action.mode),
|
|
};
|
|
case "setError":
|
|
return {
|
|
...state,
|
|
creationIntentError: action.error,
|
|
};
|
|
case "updateIntentValue":
|
|
return {
|
|
...state,
|
|
creationIntentValues: {
|
|
...state.creationIntentValues,
|
|
[action.key]: action.value,
|
|
},
|
|
creationIntentError: "",
|
|
};
|
|
case "goIntentStep":
|
|
return {
|
|
...state,
|
|
step: "intent",
|
|
creationIntentError: "",
|
|
};
|
|
default:
|
|
return state;
|
|
}
|
|
}
|
|
|
|
function parseCreationModeFromMetadata(metadata: unknown): CreationMode | null {
|
|
if (!metadata || typeof metadata !== "object") {
|
|
return null;
|
|
}
|
|
const mode = (metadata as Record<string, unknown>).creationMode;
|
|
return isCreationMode(mode) ? mode : null;
|
|
}
|
|
|
|
function useWorkspaceProjectsRootLoader(
|
|
setWorkspaceProjectsRoot: Dispatch<SetStateAction<string>>,
|
|
): void {
|
|
useEffect(() => {
|
|
let mounted = true;
|
|
|
|
const loadWorkspaceRoot = async () => {
|
|
try {
|
|
const root = await getWorkspaceProjectsRoot();
|
|
if (mounted) {
|
|
setWorkspaceProjectsRoot(root);
|
|
}
|
|
} catch (error) {
|
|
console.error("加载 workspace 目录失败:", error);
|
|
}
|
|
};
|
|
|
|
void loadWorkspaceRoot();
|
|
|
|
return () => {
|
|
mounted = false;
|
|
};
|
|
}, [setWorkspaceProjectsRoot]);
|
|
}
|
|
|
|
interface UseProjectPathResolverParams {
|
|
createProjectDialogOpen: boolean;
|
|
newProjectName: string;
|
|
resetProjectPathState: () => void;
|
|
setResolvedProjectPath: Dispatch<SetStateAction<string>>;
|
|
}
|
|
|
|
function useProjectPathResolver({
|
|
createProjectDialogOpen,
|
|
newProjectName,
|
|
resetProjectPathState,
|
|
setResolvedProjectPath,
|
|
}: UseProjectPathResolverParams): void {
|
|
useEffect(() => {
|
|
if (!createProjectDialogOpen) {
|
|
resetProjectPathState();
|
|
return;
|
|
}
|
|
|
|
const projectName = newProjectName.trim();
|
|
if (!projectName) {
|
|
resetProjectPathState();
|
|
return;
|
|
}
|
|
|
|
let mounted = true;
|
|
const resolvePath = async () => {
|
|
try {
|
|
const path = await resolveProjectRootPath(projectName);
|
|
if (mounted) {
|
|
setResolvedProjectPath(path);
|
|
}
|
|
} catch (error) {
|
|
console.error("解析项目目录失败:", error);
|
|
if (mounted) {
|
|
resetProjectPathState();
|
|
}
|
|
}
|
|
};
|
|
|
|
void resolvePath();
|
|
|
|
return () => {
|
|
mounted = false;
|
|
};
|
|
}, [
|
|
createProjectDialogOpen,
|
|
newProjectName,
|
|
resetProjectPathState,
|
|
setResolvedProjectPath,
|
|
]);
|
|
}
|
|
|
|
interface UseProjectPathConflictCheckerParams {
|
|
createProjectDialogOpen: boolean;
|
|
resolvedProjectPath: string;
|
|
setPathChecking: Dispatch<SetStateAction<boolean>>;
|
|
setPathConflictMessage: Dispatch<SetStateAction<string>>;
|
|
}
|
|
|
|
function useProjectPathConflictChecker({
|
|
createProjectDialogOpen,
|
|
resolvedProjectPath,
|
|
setPathChecking,
|
|
setPathConflictMessage,
|
|
}: UseProjectPathConflictCheckerParams): void {
|
|
useEffect(() => {
|
|
if (!createProjectDialogOpen || !resolvedProjectPath) {
|
|
setPathChecking(false);
|
|
setPathConflictMessage("");
|
|
return;
|
|
}
|
|
|
|
let mounted = true;
|
|
setPathChecking(true);
|
|
|
|
const checkPathConflict = async () => {
|
|
try {
|
|
const existingProject = await getProjectByRootPath(resolvedProjectPath);
|
|
if (!mounted) {
|
|
return;
|
|
}
|
|
if (existingProject) {
|
|
setPathConflictMessage(`路径已存在项目:${existingProject.name}`);
|
|
} else {
|
|
setPathConflictMessage("");
|
|
}
|
|
} catch (error) {
|
|
console.error("检查项目路径冲突失败:", error);
|
|
if (mounted) {
|
|
setPathConflictMessage("");
|
|
}
|
|
} finally {
|
|
if (mounted) {
|
|
setPathChecking(false);
|
|
}
|
|
}
|
|
};
|
|
|
|
void checkPathConflict();
|
|
|
|
return () => {
|
|
mounted = false;
|
|
};
|
|
}, [
|
|
createProjectDialogOpen,
|
|
resolvedProjectPath,
|
|
setPathChecking,
|
|
setPathConflictMessage,
|
|
]);
|
|
}
|
|
|
|
interface UseContentCreationModeLoaderParams {
|
|
selectedContentId: string | null;
|
|
contentCreationModes: Record<string, CreationMode>;
|
|
setContentCreationModes: Dispatch<SetStateAction<Record<string, CreationMode>>>;
|
|
}
|
|
|
|
function useContentCreationModeLoader({
|
|
selectedContentId,
|
|
contentCreationModes,
|
|
setContentCreationModes,
|
|
}: UseContentCreationModeLoaderParams): void {
|
|
useEffect(() => {
|
|
if (!selectedContentId || contentCreationModes[selectedContentId]) {
|
|
return;
|
|
}
|
|
|
|
let mounted = true;
|
|
const loadCreationMode = async () => {
|
|
try {
|
|
const content = await getContent(selectedContentId);
|
|
const mode = parseCreationModeFromMetadata(content?.metadata);
|
|
|
|
if (mounted && mode) {
|
|
setContentCreationModes((previous) => ({
|
|
...previous,
|
|
[selectedContentId]: mode,
|
|
}));
|
|
}
|
|
} catch (error) {
|
|
console.error("读取文稿创作模式失败:", error);
|
|
}
|
|
};
|
|
|
|
void loadCreationMode();
|
|
|
|
return () => {
|
|
mounted = false;
|
|
};
|
|
}, [contentCreationModes, selectedContentId, setContentCreationModes]);
|
|
}
|
|
|
|
export interface UseCreationDialogsParams {
|
|
theme: WorkspaceTheme;
|
|
selectedProjectId: string | null;
|
|
selectedContentId: string | null;
|
|
loadProjects: () => Promise<void>;
|
|
loadContents: (projectId: string) => Promise<void>;
|
|
onEnterWorkspace: (
|
|
contentId: string,
|
|
options?: {
|
|
showChatPanel?: boolean;
|
|
},
|
|
) => void;
|
|
onProjectCreated: (projectId: string) => void;
|
|
defaultCreationMode: CreationMode;
|
|
minCreationIntentLength: number;
|
|
}
|
|
|
|
export function useCreationDialogs({
|
|
theme,
|
|
selectedProjectId,
|
|
selectedContentId,
|
|
loadProjects,
|
|
loadContents,
|
|
onEnterWorkspace,
|
|
onProjectCreated,
|
|
defaultCreationMode,
|
|
minCreationIntentLength,
|
|
}: UseCreationDialogsParams) {
|
|
const [createProjectDialogOpen, setCreateProjectDialogOpen] = useState(false);
|
|
const [createContentDialogOpen, setCreateContentDialogOpen] = useState(false);
|
|
const [createContentDialogState, dispatchCreateContentDialog] = useReducer(
|
|
createContentDialogReducer,
|
|
defaultCreationMode,
|
|
createInitialContentDialogState,
|
|
);
|
|
const [newProjectName, setNewProjectName] = useState("");
|
|
const [workspaceProjectsRoot, setWorkspaceProjectsRoot] = useState("");
|
|
const [creatingProject, setCreatingProject] = useState(false);
|
|
const [creatingContent, setCreatingContent] = useState(false);
|
|
const [resolvedProjectPath, setResolvedProjectPath] = useState("");
|
|
const [pathChecking, setPathChecking] = useState(false);
|
|
const [pathConflictMessage, setPathConflictMessage] = useState("");
|
|
const [pendingInitialPromptsByContentId, setPendingInitialPromptsByContentId] =
|
|
useState<Record<string, string>>({});
|
|
const [contentCreationModes, setContentCreationModes] = useState<
|
|
Record<string, CreationMode>
|
|
>({});
|
|
|
|
const resetProjectPathState = useCallback(() => {
|
|
setResolvedProjectPath("");
|
|
setPathChecking(false);
|
|
setPathConflictMessage("");
|
|
}, []);
|
|
|
|
const creationIntentInput = useMemo<CreationIntentInput>(
|
|
() => ({
|
|
creationMode: createContentDialogState.selectedCreationMode,
|
|
values: createContentDialogState.creationIntentValues,
|
|
}),
|
|
[createContentDialogState.creationIntentValues, createContentDialogState.selectedCreationMode],
|
|
);
|
|
|
|
const currentCreationIntentFields = useMemo(
|
|
() => getCreationIntentFieldsSafe(createContentDialogState.selectedCreationMode),
|
|
[createContentDialogState.selectedCreationMode],
|
|
);
|
|
|
|
const currentIntentLength = useMemo(
|
|
() =>
|
|
validateCreationIntent(creationIntentInput, minCreationIntentLength).length,
|
|
[creationIntentInput, minCreationIntentLength],
|
|
);
|
|
|
|
const resetCreateContentDialogState = useCallback(() => {
|
|
dispatchCreateContentDialog({
|
|
type: "reset",
|
|
defaultMode: defaultCreationMode,
|
|
});
|
|
}, [defaultCreationMode]);
|
|
|
|
const setCreateContentDialogStep = useCallback(
|
|
(step: CreateContentDialogStep) => {
|
|
dispatchCreateContentDialog({
|
|
type: "setStep",
|
|
step,
|
|
});
|
|
},
|
|
[],
|
|
);
|
|
|
|
const setSelectedCreationMode = useCallback((mode: CreationMode) => {
|
|
dispatchCreateContentDialog({
|
|
type: "setMode",
|
|
mode,
|
|
});
|
|
}, []);
|
|
|
|
const setCreationIntentError = useCallback((error: string) => {
|
|
dispatchCreateContentDialog({
|
|
type: "setError",
|
|
error,
|
|
});
|
|
}, []);
|
|
|
|
const handleOpenCreateProjectDialog = useCallback(() => {
|
|
setNewProjectName(`${getProjectTypeLabel(theme as ProjectType)}项目`);
|
|
resetProjectPathState();
|
|
setCreateProjectDialogOpen(true);
|
|
}, [resetProjectPathState, theme]);
|
|
|
|
const handleCreateProject = useCallback(async () => {
|
|
const name = newProjectName.trim();
|
|
|
|
if (!name) {
|
|
toast.error("请输入项目名称");
|
|
return;
|
|
}
|
|
|
|
setCreatingProject(true);
|
|
try {
|
|
const rootPath = await resolveProjectRootPath(name);
|
|
const createdProject = await createProject({
|
|
name,
|
|
rootPath,
|
|
workspaceType: theme as ProjectType,
|
|
});
|
|
setCreateProjectDialogOpen(false);
|
|
onProjectCreated(createdProject.id);
|
|
toast.success("已创建新项目");
|
|
await loadProjects();
|
|
} catch (error) {
|
|
console.error("创建项目失败:", error);
|
|
void reportFrontendError(error, {
|
|
component: "useCreationDialogs",
|
|
workflow_step: "workspace_creation_create_project",
|
|
});
|
|
const errorMessage = extractErrorMessage(error);
|
|
const friendlyMessage = getCreateProjectErrorMessage(errorMessage);
|
|
toast.error(`创建项目失败: ${friendlyMessage}`);
|
|
} finally {
|
|
setCreatingProject(false);
|
|
}
|
|
}, [loadProjects, newProjectName, onProjectCreated, theme]);
|
|
|
|
const handleOpenCreateContentDialog = useCallback(() => {
|
|
if (!selectedProjectId) {
|
|
return;
|
|
}
|
|
resetCreateContentDialogState();
|
|
setCreateContentDialogOpen(true);
|
|
}, [resetCreateContentDialogState, selectedProjectId]);
|
|
|
|
const handleCreationIntentValueChange = useCallback(
|
|
(key: CreationIntentFieldKey, value: string) => {
|
|
dispatchCreateContentDialog({
|
|
type: "updateIntentValue",
|
|
key,
|
|
value,
|
|
});
|
|
},
|
|
[],
|
|
);
|
|
|
|
const handleGoToIntentStep = useCallback(() => {
|
|
const fields = getCreationIntentFieldsSafe(
|
|
createContentDialogState.selectedCreationMode,
|
|
);
|
|
const hasOnlyFallbackTopicField =
|
|
fields.length === 1 && fields[0]?.key === "topic";
|
|
if (
|
|
hasOnlyFallbackTopicField &&
|
|
createContentDialogState.selectedCreationMode !== "guided"
|
|
) {
|
|
console.warn(
|
|
"[useCreationDialogs] 检测到创作模式字段异常,已降级为引导模式",
|
|
{
|
|
mode: createContentDialogState.selectedCreationMode,
|
|
fieldCount: fields.length,
|
|
},
|
|
);
|
|
dispatchCreateContentDialog({
|
|
type: "setMode",
|
|
mode: "guided",
|
|
});
|
|
}
|
|
|
|
dispatchCreateContentDialog({
|
|
type: "goIntentStep",
|
|
});
|
|
}, [createContentDialogState.selectedCreationMode]);
|
|
|
|
const handleCreateContent = useCallback(async () => {
|
|
if (!selectedProjectId) {
|
|
return;
|
|
}
|
|
|
|
const validation = validateCreationIntent(
|
|
creationIntentInput,
|
|
minCreationIntentLength,
|
|
);
|
|
if (!validation.valid) {
|
|
setCreationIntentError(validation.message || "请完善创作意图");
|
|
return;
|
|
}
|
|
|
|
const initialUserPrompt = buildCreationIntentPrompt(creationIntentInput);
|
|
const creationIntentMetadata =
|
|
buildCreationIntentMetadata(creationIntentInput);
|
|
|
|
setCreatingContent(true);
|
|
try {
|
|
const defaultType = getDefaultContentTypeForProject(theme as ProjectType);
|
|
const created = await createContent({
|
|
project_id: selectedProjectId,
|
|
title: `新${getContentTypeLabel(defaultType)}`,
|
|
content_type: defaultType,
|
|
metadata: {
|
|
creationMode: createContentDialogState.selectedCreationMode,
|
|
creationIntent: creationIntentMetadata,
|
|
},
|
|
});
|
|
|
|
setContentCreationModes((previous) => ({
|
|
...previous,
|
|
[created.id]: createContentDialogState.selectedCreationMode,
|
|
}));
|
|
setPendingInitialPromptsByContentId((previous) => ({
|
|
...previous,
|
|
[created.id]: initialUserPrompt,
|
|
}));
|
|
setCreateContentDialogOpen(false);
|
|
resetCreateContentDialogState();
|
|
await loadContents(selectedProjectId);
|
|
onEnterWorkspace(created.id, { showChatPanel: true });
|
|
toast.success("已创建新文稿");
|
|
} catch (error) {
|
|
console.error("创建文稿失败:", error);
|
|
void reportFrontendError(error, {
|
|
component: "useCreationDialogs",
|
|
workflow_step: "workspace_creation_submit_intent",
|
|
creation_mode: createContentDialogState.selectedCreationMode,
|
|
});
|
|
toast.error("创建文稿失败");
|
|
} finally {
|
|
setCreatingContent(false);
|
|
}
|
|
}, [
|
|
creationIntentInput,
|
|
loadContents,
|
|
minCreationIntentLength,
|
|
onEnterWorkspace,
|
|
resetCreateContentDialogState,
|
|
createContentDialogState.selectedCreationMode,
|
|
selectedProjectId,
|
|
setCreationIntentError,
|
|
theme,
|
|
]);
|
|
|
|
const consumePendingInitialPrompt = useCallback((contentId: string) => {
|
|
setPendingInitialPromptsByContentId((previous) => {
|
|
if (!previous[contentId]) {
|
|
return previous;
|
|
}
|
|
const next = { ...previous };
|
|
delete next[contentId];
|
|
return next;
|
|
});
|
|
}, []);
|
|
|
|
useWorkspaceProjectsRootLoader(setWorkspaceProjectsRoot);
|
|
useProjectPathResolver({
|
|
createProjectDialogOpen,
|
|
newProjectName,
|
|
resetProjectPathState,
|
|
setResolvedProjectPath,
|
|
});
|
|
useProjectPathConflictChecker({
|
|
createProjectDialogOpen,
|
|
resolvedProjectPath,
|
|
setPathChecking,
|
|
setPathConflictMessage,
|
|
});
|
|
useContentCreationModeLoader({
|
|
selectedContentId,
|
|
contentCreationModes,
|
|
setContentCreationModes,
|
|
});
|
|
|
|
return {
|
|
createProjectDialogOpen,
|
|
setCreateProjectDialogOpen,
|
|
createContentDialogOpen,
|
|
setCreateContentDialogOpen,
|
|
createContentDialogStep: createContentDialogState.step,
|
|
setCreateContentDialogStep,
|
|
newProjectName,
|
|
setNewProjectName,
|
|
workspaceProjectsRoot,
|
|
creatingProject,
|
|
creatingContent,
|
|
selectedCreationMode: createContentDialogState.selectedCreationMode,
|
|
setSelectedCreationMode,
|
|
creationIntentValues: createContentDialogState.creationIntentValues,
|
|
creationIntentError: createContentDialogState.creationIntentError,
|
|
setCreationIntentError,
|
|
currentCreationIntentFields,
|
|
currentIntentLength,
|
|
pendingInitialPromptsByContentId,
|
|
contentCreationModes,
|
|
resolvedProjectPath,
|
|
pathChecking,
|
|
pathConflictMessage,
|
|
resetCreateContentDialogState,
|
|
handleOpenCreateProjectDialog,
|
|
handleCreateProject,
|
|
handleOpenCreateContentDialog,
|
|
handleCreationIntentValueChange,
|
|
handleGoToIntentStep,
|
|
handleCreateContent,
|
|
consumePendingInitialPrompt,
|
|
};
|
|
}
|
|
|
|
export default useCreationDialogs;
|