fix: centralize UUID generation and validate query thread_id for skillBootstrap; ensure single execution and correct thread dir
This commit is contained in:
parent
5aa38ee108
commit
5de7a2ab46
|
|
@ -1,6 +1,7 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import { useSearchParams } from "next/navigation";
|
||||||
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
|
|
||||||
import { type PromptInputMessage } from "@/components/ai-elements/prompt-input";
|
import { type PromptInputMessage } from "@/components/ai-elements/prompt-input";
|
||||||
import { ArtifactTrigger } from "@/components/workspace/artifacts";
|
import { ArtifactTrigger } from "@/components/workspace/artifacts";
|
||||||
|
|
@ -24,15 +25,29 @@ import { Welcome } from "@/components/workspace/welcome";
|
||||||
import { useI18n } from "@/core/i18n/hooks";
|
import { useI18n } from "@/core/i18n/hooks";
|
||||||
import { useNotification } from "@/core/notification/hooks";
|
import { useNotification } from "@/core/notification/hooks";
|
||||||
import { useThreadSettings } from "@/core/settings";
|
import { useThreadSettings } from "@/core/settings";
|
||||||
|
import { bootstrapRemoteSkill } from "@/core/skills";
|
||||||
import { useThreadStream } from "@/core/threads/hooks";
|
import { useThreadStream } from "@/core/threads/hooks";
|
||||||
import { textOfMessage } from "@/core/threads/utils";
|
import { textOfMessage } from "@/core/threads/utils";
|
||||||
|
import { uuid } from "@/core/utils/uuid";
|
||||||
import { env } from "@/env";
|
import { env } from "@/env";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const UUID_REGEX =
|
||||||
|
/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
||||||
|
|
||||||
export default function ChatPage() {
|
export default function ChatPage() {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const [showFollowups, setShowFollowups] = useState(false);
|
const [showFollowups, setShowFollowups] = useState(false);
|
||||||
const { threadId, isNewThread, setIsNewThread, isMock } = useThreadChat();
|
const searchParams = useSearchParams(); const generatedThreadIdRef = useRef<string>("");
|
||||||
|
if (!generatedThreadIdRef.current) {
|
||||||
|
const queryThreadId = searchParams.get("thread_id")?.trim();
|
||||||
|
generatedThreadIdRef.current =
|
||||||
|
queryThreadId && UUID_REGEX.test(queryThreadId) ? queryThreadId : uuid();
|
||||||
|
}
|
||||||
|
|
||||||
|
const { threadId, isNewThread, setIsNewThread, isMock } = useThreadChat({
|
||||||
|
newThreadId: generatedThreadIdRef.current,
|
||||||
|
});
|
||||||
const [settings, setSettings] = useThreadSettings(threadId);
|
const [settings, setSettings] = useThreadSettings(threadId);
|
||||||
const [mounted, setMounted] = useState(false);
|
const [mounted, setMounted] = useState(false);
|
||||||
useSpecificChatMode();
|
useSpecificChatMode();
|
||||||
|
|
@ -42,6 +57,26 @@ export default function ChatPage() {
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const { showNotification } = useNotification();
|
const { showNotification } = useNotification();
|
||||||
|
const skillBootstrappedKeyRef = useRef<string | null>(null);
|
||||||
|
const isBootstrappingRef = useRef(false);
|
||||||
|
|
||||||
|
const skillBootstrap = useMemo(() => {
|
||||||
|
const skillIdRaw = searchParams.get("skill_id")?.trim();
|
||||||
|
if (!skillIdRaw) return undefined;
|
||||||
|
|
||||||
|
const contentId = Number(skillIdRaw);
|
||||||
|
if (!Number.isFinite(contentId)) return undefined;
|
||||||
|
|
||||||
|
const languageTypeRaw =
|
||||||
|
searchParams.get("languageType")?.trim() ??
|
||||||
|
searchParams.get("language_type")?.trim();
|
||||||
|
const languageType = languageTypeRaw ? Number(languageTypeRaw) : 0;
|
||||||
|
|
||||||
|
return {
|
||||||
|
contentId,
|
||||||
|
languageType: Number.isFinite(languageType) ? languageType : 0,
|
||||||
|
};
|
||||||
|
}, [searchParams]);
|
||||||
|
|
||||||
const [thread, sendMessage, isUploading] = useThreadStream({
|
const [thread, sendMessage, isUploading] = useThreadStream({
|
||||||
threadId: isNewThread ? undefined : threadId,
|
threadId: isNewThread ? undefined : threadId,
|
||||||
|
|
@ -70,6 +105,52 @@ export default function ChatPage() {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!threadId || !skillBootstrap?.contentId) {
|
||||||
|
isBootstrappingRef.current = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const languageType = skillBootstrap.languageType ?? 0;
|
||||||
|
const initKey = `${threadId}:${skillBootstrap.contentId}:${languageType}`;
|
||||||
|
if (skillBootstrappedKeyRef.current === initKey || isBootstrappingRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let cancelled = false;
|
||||||
|
|
||||||
|
const runBootstrap = async () => {
|
||||||
|
isBootstrappingRef.current = true;
|
||||||
|
try {
|
||||||
|
await bootstrapRemoteSkill({
|
||||||
|
thread_id: threadId,
|
||||||
|
content_id: skillBootstrap.contentId,
|
||||||
|
language_type: languageType,
|
||||||
|
target_dir: "/mnt/user-data/uploads/skill",
|
||||||
|
clear_target: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!cancelled) {
|
||||||
|
skillBootstrappedKeyRef.current = initKey;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (!cancelled) {
|
||||||
|
const message =
|
||||||
|
error instanceof Error ? error.message : "Skill initialization failed";
|
||||||
|
showNotification("Skill initialization failed", { body: message });
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
isBootstrappingRef.current = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
void runBootstrap();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, [threadId, skillBootstrap, showNotification]);
|
||||||
|
|
||||||
const handleSubmit = useCallback(
|
const handleSubmit = useCallback(
|
||||||
(message: PromptInputMessage) => {
|
(message: PromptInputMessage) => {
|
||||||
void sendMessage(threadId, message);
|
void sendMessage(threadId, message);
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,23 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useParams, usePathname, useSearchParams } from "next/navigation";
|
import { useParams, usePathname, useSearchParams } from "next/navigation";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
import { uuid } from "@/core/utils/uuid";
|
import { uuid } from "@/core/utils/uuid";
|
||||||
|
|
||||||
export function useThreadChat() {
|
type UseThreadChatOptions = {
|
||||||
|
newThreadId?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function useThreadChat(options?: UseThreadChatOptions) {
|
||||||
const { thread_id: threadIdFromPath } = useParams<{ thread_id: string }>();
|
const { thread_id: threadIdFromPath } = useParams<{ thread_id: string }>();
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
|
const fallbackNewThreadIdRef = useRef<string>(options?.newThreadId ?? uuid());
|
||||||
|
const fallbackNewThreadId = options?.newThreadId ?? fallbackNewThreadIdRef.current;
|
||||||
|
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
const [threadId, setThreadId] = useState(() => {
|
const [threadId, setThreadId] = useState(() => {
|
||||||
return threadIdFromPath === "new" ? uuid() : threadIdFromPath;
|
return threadIdFromPath === "new" ? fallbackNewThreadId : threadIdFromPath;
|
||||||
});
|
});
|
||||||
|
|
||||||
const [isNewThread, setIsNewThread] = useState(
|
const [isNewThread, setIsNewThread] = useState(
|
||||||
|
|
@ -21,9 +27,9 @@ export function useThreadChat() {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (pathname.endsWith("/new")) {
|
if (pathname.endsWith("/new")) {
|
||||||
setIsNewThread(true);
|
setIsNewThread(true);
|
||||||
setThreadId(uuid());
|
setThreadId(fallbackNewThreadId);
|
||||||
}
|
}
|
||||||
}, [pathname]);
|
}, [pathname, fallbackNewThreadId]);
|
||||||
const isMock = searchParams.get("mock") === "true";
|
const isMock = searchParams.get("mock") === "true";
|
||||||
return { threadId, isNewThread, setIsNewThread, isMock };
|
return { threadId, isNewThread, setIsNewThread, isMock };
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue