wip: snapshot das alterações não commitadas (2026-10-20)
Origem: D:/Documents/Github/LocalAiApp Arquivos omitidos (segredos, bancos de dados, logs, >50 MB): 0
This commit is contained in:
1 parent
ac215b4229
commit
e08189f640
17 files changed
+2914
-13236
No files matched your search
+116
-14
@@ -3,6 +3,7 @@ import { SYSTEM_PROMPT } from '../constants/SystemPrompt';
|
||||
import * as api from '../services/lmStudioApi';
|
||||
import { ChatMessage as ApiMessage } from '../services/types';
|
||||
import { useAppStore } from '../store/useAppStore';
|
||||
import { WebSearchLogger } from '../services/webSearchLogger';
|
||||
|
||||
export function useLMStudio() {
|
||||
const serverConfig = useAppStore((s) => s.serverConfig);
|
||||
@@ -47,18 +48,13 @@ export function useLMStudio() {
|
||||
const sendMessage = useCallback(async (text: string) => {
|
||||
if (isStreaming) return;
|
||||
|
||||
const pipelineStart = Date.now();
|
||||
const store = useAppStore.getState();
|
||||
const isWebSearchEnabled = store.isWebSearchEnabled;
|
||||
|
||||
addMessage({ role: 'user', content: text });
|
||||
|
||||
const currentMessages = useAppStore.getState().messages;
|
||||
const apiMessages: ApiMessage[] = [
|
||||
{ role: 'system', content: SYSTEM_PROMPT },
|
||||
...currentMessages.map((m) => ({
|
||||
role: m.role,
|
||||
content: m.content,
|
||||
}))
|
||||
];
|
||||
|
||||
const modelId = useAppStore.getState().activeModelId;
|
||||
const modelId = store.activeModelId;
|
||||
if (!modelId) {
|
||||
addMessage({
|
||||
role: 'assistant',
|
||||
@@ -71,19 +67,128 @@ export function useLMStudio() {
|
||||
setIsStreaming(true);
|
||||
|
||||
try {
|
||||
let searchContext = '';
|
||||
let searchSources: Array<{ title: string; url: string }> = [];
|
||||
|
||||
if (isWebSearchEnabled) {
|
||||
// ── Step 1: Build search service URL ─────────────────────────────
|
||||
// ServerConfig has { host, port } — construct the search service URL
|
||||
// from the same host, but on port 3030.
|
||||
const rawHost = store.serverConfig.host.replace(/\/+$/, '');
|
||||
const hostWithProtocol = rawHost.startsWith('http') ? rawHost : `http://${rawHost}`;
|
||||
const searchEndpoint = `${hostWithProtocol}:3030/search-and-extract`;
|
||||
|
||||
WebSearchLogger.pipelineStart(text, searchEndpoint);
|
||||
WebSearchLogger.endpointResolved(
|
||||
store.serverConfig.host,
|
||||
store.serverConfig.port,
|
||||
searchEndpoint,
|
||||
);
|
||||
|
||||
appendToMessage(assistantId, '<think>Searching the web...</think>\n');
|
||||
|
||||
try {
|
||||
const maxContextLength = 10000;
|
||||
// 60s timeout: SearXNG + 3 páginas em paralelo (max ~8s cada) + parsing
|
||||
// Sequential era ~24s worst case; paralelo cabe em ~10-15s. 60s é margem segura.
|
||||
const SEARCH_TIMEOUT_MS = 60000;
|
||||
const searchController = new AbortController();
|
||||
const searchTimeout = setTimeout(() => {
|
||||
searchController.abort();
|
||||
}, SEARCH_TIMEOUT_MS);
|
||||
|
||||
WebSearchLogger.requestSent(text, maxContextLength, SEARCH_TIMEOUT_MS);
|
||||
|
||||
const searchRes = await fetch(searchEndpoint, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
query: text,
|
||||
maxContextLength,
|
||||
}),
|
||||
signal: searchController.signal,
|
||||
});
|
||||
|
||||
clearTimeout(searchTimeout);
|
||||
|
||||
if (searchRes.ok) {
|
||||
const searchData = await searchRes.json();
|
||||
searchContext = searchData.context ?? '';
|
||||
searchSources = searchData.sources ?? [];
|
||||
|
||||
WebSearchLogger.contextReceived(
|
||||
searchContext,
|
||||
searchSources,
|
||||
searchData.meta,
|
||||
);
|
||||
} else {
|
||||
WebSearchLogger.requestFailed(searchRes.status, searchRes.statusText);
|
||||
}
|
||||
} catch (error) {
|
||||
const isTimeout = error instanceof Error && error.name === 'AbortError';
|
||||
WebSearchLogger.serviceUnreachable(error, isTimeout);
|
||||
}
|
||||
|
||||
// Clear the temporary "searching..." thought bubble
|
||||
store.updateMessageContent(assistantId, '');
|
||||
}
|
||||
|
||||
// ── Step 2: Build API messages ──────────────────────────────────────
|
||||
const currentMessages = useAppStore.getState().messages;
|
||||
// Exclude the empty assistant placeholder from the history
|
||||
const historyToSend = currentMessages.slice(0, currentMessages.length - 1);
|
||||
|
||||
// Warn explicitly when web search was enabled but returned no context
|
||||
if (isWebSearchEnabled && !searchContext) {
|
||||
console.warn(
|
||||
'[WebSearch/Fallback] ⚠️ Web search was enabled but returned NO context. ' +
|
||||
'Proceeding with LLM response without web data. ' +
|
||||
'Check if local-search-service is running at the correct address.',
|
||||
);
|
||||
}
|
||||
|
||||
const systemContent = SYSTEM_PROMPT + (searchContext ? `\n\n${searchContext}` : '');
|
||||
|
||||
const apiMessages: ApiMessage[] = [
|
||||
{ role: 'system', content: systemContent },
|
||||
...historyToSend.map((m) => ({
|
||||
role: m.role,
|
||||
content: m.content,
|
||||
})),
|
||||
];
|
||||
|
||||
// ── Step 3: Context Window Analysis ────────────────────────────────
|
||||
if (isWebSearchEnabled) {
|
||||
const historyChars = historyToSend.reduce((sum, m) => sum + m.content.length, 0);
|
||||
WebSearchLogger.contextAnalysis(
|
||||
systemContent.length,
|
||||
historyChars,
|
||||
searchContext.length,
|
||||
);
|
||||
}
|
||||
|
||||
// ── Step 4: Stream LLM response ────────────────────────────────────
|
||||
WebSearchLogger.llmRequestStart(modelId, apiMessages.length);
|
||||
|
||||
let chunkCount = 0;
|
||||
|
||||
abortRef.current = api.chatCompletionStream(
|
||||
serverConfig,
|
||||
modelId,
|
||||
apiMessages,
|
||||
(chunk) => {
|
||||
chunkCount++;
|
||||
appendToMessage(assistantId, chunk);
|
||||
},
|
||||
() => {
|
||||
WebSearchLogger.llmStreamComplete(assistantId, chunkCount);
|
||||
WebSearchLogger.pipelineComplete(text, Date.now() - pipelineStart, isWebSearchEnabled);
|
||||
setIsStreaming(false);
|
||||
abortRef.current = null;
|
||||
useAppStore.getState().saveCurrentChat();
|
||||
},
|
||||
(error) => {
|
||||
WebSearchLogger.llmError(error);
|
||||
const errorMsg = `\n\n⚠️ Error: ${error.message}`;
|
||||
appendToMessage(assistantId, errorMsg);
|
||||
setIsStreaming(false);
|
||||
@@ -103,7 +208,6 @@ export function useLMStudio() {
|
||||
if (isStreaming) return;
|
||||
|
||||
const msgs = useAppStore.getState().messages;
|
||||
// Find the last user message
|
||||
let lastUserIdx = -1;
|
||||
for (let i = msgs.length - 1; i >= 0; i--) {
|
||||
if (msgs[i].role === 'user') {
|
||||
@@ -114,18 +218,16 @@ export function useLMStudio() {
|
||||
if (lastUserIdx === -1) return;
|
||||
|
||||
const lastUserMsg = msgs[lastUserIdx];
|
||||
// Remove messages after the last user message
|
||||
const store = useAppStore.getState();
|
||||
store.deleteMessagesAfter(lastUserMsg.id);
|
||||
|
||||
// Build API messages from remaining
|
||||
const remaining = useAppStore.getState().messages;
|
||||
const apiMessages: ApiMessage[] = [
|
||||
{ role: 'system', content: SYSTEM_PROMPT },
|
||||
...remaining.map((m) => ({
|
||||
role: m.role,
|
||||
content: m.content,
|
||||
}))
|
||||
})),
|
||||
];
|
||||
|
||||
const modelId = useAppStore.getState().activeModelId;
|
||||
|
||||
Reference in new issue
Block a user