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.
141 lines
5.0 KiB
TypeScript
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,
|
|
};
|
|
}
|