feat: Go backend, enhanced search, new widgets, Docker deploy
Major changes: - Add Go backend (backend/) with microservices architecture - Enhanced master-agents-svc: reranker, content-classifier, stealth-crawler, proxy-manager, media-search, fastClassifier, language detection - New web-svc widgets: KnowledgeCard, ProductCard, ProfileCard, VideoCard, UnifiedCard, CardGallery, InlineImageGallery, SourcesPanel, RelatedQuestions - Improved discover-svc with discover-db integration - Docker deployment improvements (Caddyfile, vendor.sh, BUILD.md) - Library-svc: project_id schema migration - Remove deprecated finance-svc and travel-svc - Localization improvements across services Made-with: Cursor
This commit is contained in:
177
backend/webui/src/lib/hooks/useChat.ts
Normal file
177
backend/webui/src/lib/hooks/useChat.ts
Normal file
@@ -0,0 +1,177 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useCallback, useRef } from 'react';
|
||||
import type { Message, Citation, Widget, StreamEvent } from '../types';
|
||||
import { streamChat, generateId } from '../api';
|
||||
|
||||
interface UseChatOptions {
|
||||
onError?: (error: Error) => void;
|
||||
}
|
||||
|
||||
export function useChat(options: UseChatOptions = {}) {
|
||||
const [messages, setMessages] = useState<Message[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [citations, setCitations] = useState<Citation[]>([]);
|
||||
const [widgets, setWidgets] = useState<Widget[]>([]);
|
||||
const abortControllerRef = useRef<AbortController | null>(null);
|
||||
|
||||
const sendMessage = useCallback(async (content: string, mode: 'speed' | 'balanced' | 'quality' = 'balanced') => {
|
||||
if (!content.trim() || isLoading) return;
|
||||
|
||||
const userMessage: Message = {
|
||||
id: generateId(),
|
||||
role: 'user',
|
||||
content: content.trim(),
|
||||
createdAt: new Date(),
|
||||
};
|
||||
|
||||
const assistantMessage: Message = {
|
||||
id: generateId(),
|
||||
role: 'assistant',
|
||||
content: '',
|
||||
citations: [],
|
||||
widgets: [],
|
||||
isStreaming: true,
|
||||
createdAt: new Date(),
|
||||
};
|
||||
|
||||
setMessages((prev) => [...prev, userMessage, assistantMessage]);
|
||||
setIsLoading(true);
|
||||
setCitations([]);
|
||||
setWidgets([]);
|
||||
|
||||
const history: [string, string][] = messages
|
||||
.filter((m) => !m.isStreaming)
|
||||
.reduce((acc, m, i, arr) => {
|
||||
if (m.role === 'user' && arr[i + 1]?.role === 'assistant') {
|
||||
acc.push([m.content, arr[i + 1].content]);
|
||||
}
|
||||
return acc;
|
||||
}, [] as [string, string][]);
|
||||
|
||||
try {
|
||||
const stream = streamChat({
|
||||
message: {
|
||||
messageId: assistantMessage.id,
|
||||
chatId: userMessage.id,
|
||||
content: content.trim(),
|
||||
},
|
||||
optimizationMode: mode,
|
||||
history,
|
||||
locale: 'ru',
|
||||
});
|
||||
|
||||
let fullContent = '';
|
||||
const collectedCitations: Citation[] = [];
|
||||
const collectedWidgets: Widget[] = [];
|
||||
|
||||
for await (const event of stream) {
|
||||
switch (event.type) {
|
||||
case 'messageStart':
|
||||
break;
|
||||
|
||||
case 'message':
|
||||
if (event.data && typeof event.data === 'object' && 'content' in event.data) {
|
||||
const chunk = (event.data as { content: string }).content;
|
||||
fullContent += chunk;
|
||||
setMessages((prev) =>
|
||||
prev.map((m) =>
|
||||
m.id === assistantMessage.id
|
||||
? { ...m, content: fullContent }
|
||||
: m
|
||||
)
|
||||
);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'sources':
|
||||
if (event.data && Array.isArray(event.data)) {
|
||||
const sources = event.data as Array<{
|
||||
url: string;
|
||||
title?: string;
|
||||
metadata?: { title?: string };
|
||||
}>;
|
||||
sources.forEach((source, idx) => {
|
||||
const citation: Citation = {
|
||||
index: idx + 1,
|
||||
url: source.url,
|
||||
title: source.title || source.metadata?.title || new URL(source.url).hostname,
|
||||
domain: new URL(source.url).hostname,
|
||||
};
|
||||
collectedCitations.push(citation);
|
||||
});
|
||||
setCitations([...collectedCitations]);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'widget':
|
||||
if (event.data && typeof event.data === 'object') {
|
||||
const widget = event.data as Widget;
|
||||
collectedWidgets.push(widget);
|
||||
setWidgets([...collectedWidgets]);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'messageEnd':
|
||||
setMessages((prev) =>
|
||||
prev.map((m) =>
|
||||
m.id === assistantMessage.id
|
||||
? {
|
||||
...m,
|
||||
content: fullContent,
|
||||
citations: collectedCitations,
|
||||
widgets: collectedWidgets,
|
||||
isStreaming: false,
|
||||
}
|
||||
: m
|
||||
)
|
||||
);
|
||||
break;
|
||||
|
||||
case 'error':
|
||||
throw new Error(
|
||||
event.data && typeof event.data === 'object' && 'message' in event.data
|
||||
? String((event.data as { message: string }).message)
|
||||
: 'Unknown error'
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Ошибка при отправке сообщения';
|
||||
setMessages((prev) =>
|
||||
prev.map((m) =>
|
||||
m.id === assistantMessage.id
|
||||
? { ...m, content: `Ошибка: ${errorMessage}`, isStreaming: false }
|
||||
: m
|
||||
)
|
||||
);
|
||||
options.onError?.(error instanceof Error ? error : new Error(errorMessage));
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [isLoading, messages, options]);
|
||||
|
||||
const stopGeneration = useCallback(() => {
|
||||
abortControllerRef.current?.abort();
|
||||
setIsLoading(false);
|
||||
setMessages((prev) =>
|
||||
prev.map((m) => (m.isStreaming ? { ...m, isStreaming: false } : m))
|
||||
);
|
||||
}, []);
|
||||
|
||||
const clearMessages = useCallback(() => {
|
||||
setMessages([]);
|
||||
setCitations([]);
|
||||
setWidgets([]);
|
||||
}, []);
|
||||
|
||||
return {
|
||||
messages,
|
||||
isLoading,
|
||||
citations,
|
||||
widgets,
|
||||
sendMessage,
|
||||
stopGeneration,
|
||||
clearMessages,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user