fix: answer persistent
This commit is contained in:
@@ -69,17 +69,37 @@ interface SessionData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function ChatInterface() {
|
export function ChatInterface() {
|
||||||
const [messages, setMessages] = useState<Message[]>([]);
|
const [messagesBySession, setMessagesBySession] = useState<Record<string, Message[]>>({});
|
||||||
const [input, setInput] = useState("");
|
const [input, setInput] = useState("");
|
||||||
const [selectedDataSource, setSelectedDataSource] = useState<string>("");
|
const [selectedDataSource, setSelectedDataSource] = useState<string>("");
|
||||||
const [availableSkills, setAvailableSkills] = useState<Skill[]>([]);
|
const [availableSkills, setAvailableSkills] = useState<Skill[]>([]);
|
||||||
const [selectedSkillIds, setSelectedSkillIds] = useState<string[]>([]);
|
const [selectedSkillIds, setSelectedSkillIds] = useState<string[]>([]);
|
||||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
|
||||||
const scrollRef = useRef<HTMLDivElement>(null);
|
const scrollRef = useRef<HTMLDivElement>(null);
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const { currentProject } = useProjectStore();
|
const { currentProject } = useProjectStore();
|
||||||
|
|
||||||
|
const setMessagesForSession = (sessionKey: string, updater: React.SetStateAction<Message[]>) => {
|
||||||
|
setMessagesBySession(prev => {
|
||||||
|
const current = prev[sessionKey] || [];
|
||||||
|
const next = typeof updater === 'function' ? (updater as (msgs: Message[]) => Message[])(current) : updater;
|
||||||
|
return { ...prev, [sessionKey]: next };
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const setIsLoadingForSession = (sessionKey: string, loading: boolean) => {
|
||||||
|
setLoadingBySession(prev => ({ ...prev, [sessionKey]: loading }));
|
||||||
|
};
|
||||||
|
const queryParams = new URLSearchParams(location.search);
|
||||||
|
const activeSessionKey = queryParams.get("session") || "api:default";
|
||||||
|
|
||||||
|
const messages = messagesBySession[activeSessionKey] || [];
|
||||||
|
const [loadingBySession, setLoadingBySession] = useState<Record<string, boolean>>({});
|
||||||
|
const isLoading = loadingBySession[activeSessionKey] || false;
|
||||||
|
|
||||||
|
const generatingSessionsRef = useRef<Record<string, boolean>>({});
|
||||||
|
const abortControllersRef = useRef<Record<string, AbortController>>({});
|
||||||
|
|
||||||
// Model selection state
|
// Model selection state
|
||||||
const [models, setModels] = useState<ModelConfig[]>([]);
|
const [models, setModels] = useState<ModelConfig[]>([]);
|
||||||
const [selectedModelId, setSelectedModelId] = useState<string>("");
|
const [selectedModelId, setSelectedModelId] = useState<string>("");
|
||||||
@@ -88,16 +108,11 @@ export function ChatInterface() {
|
|||||||
// Data Source selection state
|
// Data Source selection state
|
||||||
const [availableDataSources, setAvailableDataSources] = useState<{id: string, name: string}[]>([]);
|
const [availableDataSources, setAvailableDataSources] = useState<{id: string, name: string}[]>([]);
|
||||||
|
|
||||||
// Try to parse active session from URL query
|
|
||||||
const queryParams = new URLSearchParams(location.search);
|
|
||||||
const activeSessionKey = queryParams.get("session") || "api:default";
|
|
||||||
|
|
||||||
// File upload state
|
// File upload state
|
||||||
const [attachedFile, setAttachedFile] = useState<DataFileContext | null>(null);
|
const [attachedFile, setAttachedFile] = useState<DataFileContext | null>(null);
|
||||||
const [activeDataFile, setActiveDataFile] = useState<DataFileContext | null>(null);
|
const [activeDataFile, setActiveDataFile] = useState<DataFileContext | null>(null);
|
||||||
const [isUploading, setIsUploading] = useState(false);
|
const [isUploading, setIsUploading] = useState(false);
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
const abortControllerRef = useRef<AbortController | null>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchModels();
|
fetchModels();
|
||||||
@@ -147,7 +162,10 @@ export function ChatInterface() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchSessionData = async () => {
|
const fetchSessionData = async () => {
|
||||||
setIsLoading(true);
|
if (generatingSessionsRef.current[activeSessionKey]) {
|
||||||
|
return; // Do not fetch if we are currently generating for this session
|
||||||
|
}
|
||||||
|
setIsLoadingForSession(activeSessionKey, true);
|
||||||
setSelectedSkillIds([]);
|
setSelectedSkillIds([]);
|
||||||
try {
|
try {
|
||||||
const data = await api.get<SessionData>(`/nanobot/sessions/${activeSessionKey}`);
|
const data = await api.get<SessionData>(`/nanobot/sessions/${activeSessionKey}`);
|
||||||
@@ -158,9 +176,9 @@ export function ChatInterface() {
|
|||||||
content: m.content,
|
content: m.content,
|
||||||
viz: m.viz ? buildMessageViz(m.viz) : undefined,
|
viz: m.viz ? buildMessageViz(m.viz) : undefined,
|
||||||
}));
|
}));
|
||||||
setMessages(formattedMessages);
|
setMessagesForSession(activeSessionKey, formattedMessages);
|
||||||
} else {
|
} else {
|
||||||
setMessages([]);
|
setMessagesForSession(activeSessionKey, []);
|
||||||
}
|
}
|
||||||
const restoredFile = data.metadata?.active_data_file || null;
|
const restoredFile = data.metadata?.active_data_file || null;
|
||||||
const restoredSource = data.metadata?.selected_data_source || "";
|
const restoredSource = data.metadata?.selected_data_source || "";
|
||||||
@@ -169,12 +187,12 @@ export function ChatInterface() {
|
|||||||
setAttachedFile(null);
|
setAttachedFile(null);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Failed to fetch session messages", e);
|
console.error("Failed to fetch session messages", e);
|
||||||
setMessages([]);
|
setMessagesForSession(activeSessionKey, []);
|
||||||
setActiveDataFile(null);
|
setActiveDataFile(null);
|
||||||
setSelectedDataSource("");
|
setSelectedDataSource("");
|
||||||
setAttachedFile(null);
|
setAttachedFile(null);
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoadingForSession(activeSessionKey, false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -345,11 +363,12 @@ export function ChatInterface() {
|
|||||||
}, [messages]);
|
}, [messages]);
|
||||||
|
|
||||||
const handleForceStop = () => {
|
const handleForceStop = () => {
|
||||||
const controller = abortControllerRef.current;
|
const controller = abortControllersRef.current[activeSessionKey];
|
||||||
if (!controller) return;
|
if (!controller) return;
|
||||||
controller.abort();
|
controller.abort();
|
||||||
setIsLoading(false);
|
setIsLoadingForSession(activeSessionKey, false);
|
||||||
setMessages((prev) =>
|
generatingSessionsRef.current[activeSessionKey] = false;
|
||||||
|
setMessagesForSession(activeSessionKey, (prev) =>
|
||||||
prev.map((msg) =>
|
prev.map((msg) =>
|
||||||
msg.awaitingFirstToken
|
msg.awaitingFirstToken
|
||||||
? { ...msg, awaitingFirstToken: false, content: msg.content || "已中断输出" }
|
? { ...msg, awaitingFirstToken: false, content: msg.content || "已中断输出" }
|
||||||
@@ -361,8 +380,9 @@ export function ChatInterface() {
|
|||||||
const handleSend = async () => {
|
const handleSend = async () => {
|
||||||
if (!input.trim() || isLoading) return;
|
if (!input.trim() || isLoading) return;
|
||||||
|
|
||||||
|
const targetSessionKey = activeSessionKey;
|
||||||
const newMessage: Message = { id: Date.now().toString(), role: 'user', content: input };
|
const newMessage: Message = { id: Date.now().toString(), role: 'user', content: input };
|
||||||
setMessages(prev => [...prev, newMessage]);
|
setMessagesForSession(targetSessionKey, prev => [...prev, newMessage]);
|
||||||
setInput("");
|
setInput("");
|
||||||
|
|
||||||
let messagePayload = newMessage.content;
|
let messagePayload = newMessage.content;
|
||||||
@@ -373,12 +393,13 @@ export function ChatInterface() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
abortControllerRef.current = controller;
|
abortControllersRef.current[targetSessionKey] = controller;
|
||||||
setIsLoading(true);
|
generatingSessionsRef.current[targetSessionKey] = true;
|
||||||
|
setIsLoadingForSession(targetSessionKey, true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const assistantId = (Date.now() + 1).toString();
|
const assistantId = (Date.now() + 1).toString();
|
||||||
setMessages(prev => [...prev, {
|
setMessagesForSession(targetSessionKey, prev => [...prev, {
|
||||||
id: assistantId,
|
id: assistantId,
|
||||||
role: "assistant",
|
role: "assistant",
|
||||||
content: "",
|
content: "",
|
||||||
@@ -405,7 +426,7 @@ export function ChatInterface() {
|
|||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
message: messagePayload,
|
message: messagePayload,
|
||||||
session_id: activeSessionKey,
|
session_id: targetSessionKey,
|
||||||
model_id: effectiveModelId,
|
model_id: effectiveModelId,
|
||||||
skill_ids: selectedSkillIds,
|
skill_ids: selectedSkillIds,
|
||||||
source,
|
source,
|
||||||
@@ -452,7 +473,7 @@ export function ChatInterface() {
|
|||||||
|
|
||||||
if (payload.type === "delta" && payload.content) {
|
if (payload.type === "delta" && payload.content) {
|
||||||
streamedText = `${streamedText}${payload.content}`;
|
streamedText = `${streamedText}${payload.content}`;
|
||||||
setMessages((prev) =>
|
setMessagesForSession(targetSessionKey, (prev) =>
|
||||||
prev.map((msg) =>
|
prev.map((msg) =>
|
||||||
msg.id === assistantId ? { ...msg, content: streamedText, awaitingFirstToken: false } : msg
|
msg.id === assistantId ? { ...msg, content: streamedText, awaitingFirstToken: false } : msg
|
||||||
)
|
)
|
||||||
@@ -461,7 +482,7 @@ export function ChatInterface() {
|
|||||||
|
|
||||||
if (payload.type === "final" && payload.content) {
|
if (payload.type === "final" && payload.content) {
|
||||||
streamedText = payload.content;
|
streamedText = payload.content;
|
||||||
setMessages((prev) =>
|
setMessagesForSession(targetSessionKey, (prev) =>
|
||||||
prev.map((msg) =>
|
prev.map((msg) =>
|
||||||
msg.id === assistantId ? { ...msg, content: payload.content || "", awaitingFirstToken: false, viz: streamedViz ?? msg.viz } : msg
|
msg.id === assistantId ? { ...msg, content: payload.content || "", awaitingFirstToken: false, viz: streamedViz ?? msg.viz } : msg
|
||||||
)
|
)
|
||||||
@@ -474,7 +495,7 @@ export function ChatInterface() {
|
|||||||
|
|
||||||
if (payload.type === "viz") {
|
if (payload.type === "viz") {
|
||||||
streamedViz = buildMessageViz(payload);
|
streamedViz = buildMessageViz(payload);
|
||||||
setMessages((prev) =>
|
setMessagesForSession(targetSessionKey, (prev) =>
|
||||||
prev.map((msg) =>
|
prev.map((msg) =>
|
||||||
msg.id === assistantId ? { ...msg, viz: streamedViz || undefined } : msg
|
msg.id === assistantId ? { ...msg, viz: streamedViz || undefined } : msg
|
||||||
)
|
)
|
||||||
@@ -494,7 +515,7 @@ export function ChatInterface() {
|
|||||||
};
|
};
|
||||||
}>("/nanobot/chat", {
|
}>("/nanobot/chat", {
|
||||||
message: messagePayload,
|
message: messagePayload,
|
||||||
session_id: activeSessionKey,
|
session_id: targetSessionKey,
|
||||||
model_id: effectiveModelId,
|
model_id: effectiveModelId,
|
||||||
skill_ids: selectedSkillIds,
|
skill_ids: selectedSkillIds,
|
||||||
source,
|
source,
|
||||||
@@ -503,7 +524,7 @@ export function ChatInterface() {
|
|||||||
route_mode: "auto",
|
route_mode: "auto",
|
||||||
}, { signal: controller.signal });
|
}, { signal: controller.signal });
|
||||||
const fallbackViz = fallback.viz ? buildMessageViz(fallback.viz) : undefined;
|
const fallbackViz = fallback.viz ? buildMessageViz(fallback.viz) : undefined;
|
||||||
setMessages((prev) =>
|
setMessagesForSession(targetSessionKey, (prev) =>
|
||||||
prev.map((msg) =>
|
prev.map((msg) =>
|
||||||
msg.id === assistantId ? { ...msg, content: fallback.response || "暂无回复", awaitingFirstToken: false, viz: fallbackViz } : msg
|
msg.id === assistantId ? { ...msg, content: fallback.response || "暂无回复", awaitingFirstToken: false, viz: fallbackViz } : msg
|
||||||
)
|
)
|
||||||
@@ -511,7 +532,7 @@ export function ChatInterface() {
|
|||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
if (error?.name === "AbortError" || String(error?.message || "").toLowerCase().includes("aborted")) {
|
if (error?.name === "AbortError" || String(error?.message || "").toLowerCase().includes("aborted")) {
|
||||||
setMessages((prev) =>
|
setMessagesForSession(targetSessionKey, (prev) =>
|
||||||
prev.map((msg) =>
|
prev.map((msg) =>
|
||||||
msg.awaitingFirstToken
|
msg.awaitingFirstToken
|
||||||
? { ...msg, awaitingFirstToken: false, content: msg.content || "已中断输出" }
|
? { ...msg, awaitingFirstToken: false, content: msg.content || "已中断输出" }
|
||||||
@@ -520,16 +541,17 @@ export function ChatInterface() {
|
|||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setMessages(prev => [...prev, {
|
setMessagesForSession(targetSessionKey, prev => [...prev, {
|
||||||
id: (Date.now() + 1).toString(),
|
id: (Date.now() + 1).toString(),
|
||||||
role: 'assistant',
|
role: 'assistant',
|
||||||
content: `Sorry, something went wrong: ${error.message}`
|
content: `Sorry, something went wrong: ${error.message}`
|
||||||
}]);
|
}]);
|
||||||
} finally {
|
} finally {
|
||||||
if (abortControllerRef.current === controller) {
|
if (abortControllersRef.current[targetSessionKey] === controller) {
|
||||||
abortControllerRef.current = null;
|
delete abortControllersRef.current[targetSessionKey];
|
||||||
}
|
}
|
||||||
setIsLoading(false);
|
generatingSessionsRef.current[targetSessionKey] = false;
|
||||||
|
setIsLoadingForSession(targetSessionKey, false);
|
||||||
window.dispatchEvent(new Event("nanobot:sessions-changed"));
|
window.dispatchEvent(new Event("nanobot:sessions-changed"));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user