Files
LocalMind/hooks/useLMStudio.ts
evandro 1a3ce9ccec fix(chat): do not delete the answer when regenerating without a selected model
regenerateResponse removed the previous answer before checking for a model and then returned silently, leaving the conversation without a reply. It now checks first and explains that a model is required, like sendMessage. The duplicated streaming code in sendMessage and regenerateResponse is now a single streamAnswer helper.
2026-10-10 04:31:21 -03:00

141 lines
5.0 KiB
TypeScript

import { useCallback, useRef } from 'react';
import { SYSTEM_PROMPT } from '../constants/SystemPrompt';
import * as api from '../services/lmStudioApi';
import { ChatMessage as ApiMessage } from '../services/types';
import { ChatMessage, useAppStore } from '../store/useAppStore';
const NO_MODEL_MESSAGE = '⚠️ No model selected. Please select a model first.';
function buildApiMessages(messages: ChatMessage[]): ApiMessage[] {
return [
{ role: 'system', content: SYSTEM_PROMPT },
...messages.map((m) => ({ role: m.role, content: m.content })),
];
}
export function useLMStudio() {
const serverConfig = useAppStore((s) => s.serverConfig);
const isConnected = useAppStore((s) => s.isConnected);
const setIsConnected = useAppStore((s) => s.setIsConnected);
const availableModels = useAppStore((s) => s.availableModels);
const setAvailableModels = useAppStore((s) => s.setAvailableModels);
const activeModelId = useAppStore((s) => s.activeModelId);
const setActiveModelId = useAppStore((s) => s.setActiveModelId);
const messages = useAppStore((s) => s.messages);
const addMessage = useAppStore((s) => s.addMessage);
const appendToMessage = useAppStore((s) => s.appendToMessage);
const isStreaming = useAppStore((s) => s.isStreaming);
const setIsStreaming = useAppStore((s) => s.setIsStreaming);
const abortRef = useRef<{ abort: () => void } | null>(null);
const checkConnection = useCallback(async () => {
const ok = await api.testConnection(serverConfig);
setIsConnected(ok);
return ok;
}, [serverConfig, setIsConnected]);
const fetchModels = useCallback(async () => {
try {
const currentConfig = useAppStore.getState().serverConfig;
const models = await api.getModels(currentConfig);
setAvailableModels(models);
setIsConnected(true);
if (!activeModelId && models.length > 0) {
setActiveModelId(models[0].id);
}
return models;
} catch {
setIsConnected(false);
setAvailableModels([]);
return [];
}
}, [activeModelId, setAvailableModels, setIsConnected, setActiveModelId]);
// Adds an empty assistant message and streams the model's answer into it.
const streamAnswer = useCallback((modelId: string, apiMessages: ApiMessage[]) => {
const assistantId = addMessage({ role: 'assistant', content: '' });
setIsStreaming(true);
const finish = () => {
setIsStreaming(false);
abortRef.current = null;
useAppStore.getState().saveCurrentChat();
};
try {
abortRef.current = api.chatCompletionStream(
serverConfig,
modelId,
apiMessages,
(chunk) => appendToMessage(assistantId, chunk),
finish,
(error) => {
appendToMessage(assistantId, `
⚠️ Error: ${error.message}`);
finish();
},
);
} catch (err) {
appendToMessage(assistantId, `⚠️ Connection failed: ${(err as Error).message}`);
finish();
}
}, [serverConfig, addMessage, appendToMessage, setIsStreaming]);
const sendMessage = useCallback(async (text: string) => {
if (isStreaming) return;
addMessage({ role: 'user', content: text });
const modelId = useAppStore.getState().activeModelId;
if (!modelId) {
addMessage({ role: 'assistant', content: NO_MODEL_MESSAGE });
return;
}
streamAnswer(modelId, buildApiMessages(useAppStore.getState().messages));
}, [isStreaming, addMessage, streamAnswer]);
const regenerateResponse = useCallback(async () => {
if (isStreaming) return;
const { messages: msgs, activeModelId: modelId, deleteMessagesAfter } = useAppStore.getState();
const lastUserMsg = [...msgs].reverse().find((m) => m.role === 'user');
if (!lastUserMsg) return;
// Checked before deleting anything, so the current answer is not lost for nothing.
if (!modelId) {
addMessage({ role: 'assistant', content: NO_MODEL_MESSAGE });
return;
}
// Drop the previous answer(s) and ask again from the last user message.
deleteMessagesAfter(lastUserMsg.id);
streamAnswer(modelId, buildApiMessages(useAppStore.getState().messages));
}, [isStreaming, addMessage, streamAnswer]);
const stopStreaming = useCallback(() => {
if (abortRef.current) {
abortRef.current.abort();
abortRef.current = null;
setIsStreaming(false);
useAppStore.getState().saveCurrentChat();
}
}, [setIsStreaming]);
return {
isConnected,
isStreaming,
availableModels,
activeModelId,
messages,
sendMessage,
stopStreaming,
fetchModels,
checkConnection,
regenerateResponse,
};
}