feat: streaming AI responses with stop button
This commit is contained in:
+64
-15
@@ -9,7 +9,7 @@ import {
|
||||
getPlayerCharacter,
|
||||
} from "../services/api";
|
||||
import {
|
||||
generateStoryResponse,
|
||||
generateStoryResponseStream,
|
||||
buildSystemPrompt,
|
||||
sendMessage,
|
||||
generateStorySummary,
|
||||
@@ -52,6 +52,8 @@ export default function GamePage() {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isInitialLoading, setIsInitialLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [streamingContent, setStreamingContent] = useState("");
|
||||
const abortControllerRef = useRef<AbortController | null>(null);
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
const inputRef = useRef<HTMLTextAreaElement>(null);
|
||||
|
||||
@@ -241,15 +243,23 @@ export default function GamePage() {
|
||||
setInput("");
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
setStreamingContent("");
|
||||
|
||||
// Создаём AbortController для возможности отмены
|
||||
abortControllerRef.current = new AbortController();
|
||||
|
||||
try {
|
||||
// Передаём session для оптимизированного контекста
|
||||
const response = await generateStoryResponse(
|
||||
// Streaming ответ от AI
|
||||
const response = await generateStoryResponseStream(
|
||||
story,
|
||||
session.messages,
|
||||
input.trim(),
|
||||
(chunk) => {
|
||||
setStreamingContent((prev) => prev + chunk);
|
||||
},
|
||||
playerCharacter || undefined,
|
||||
session,
|
||||
abortControllerRef.current.signal,
|
||||
);
|
||||
|
||||
const assistantMessage: ChatMessage = {
|
||||
@@ -294,16 +304,41 @@ export default function GamePage() {
|
||||
);
|
||||
setSession(finalSession);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "Произошла ошибка");
|
||||
// Откатываем сообщение пользователя при ошибке
|
||||
setSession(session);
|
||||
setInput(userMessage.content);
|
||||
if (err instanceof Error && err.name === "AbortError") {
|
||||
// Пользователь отменил — сохраняем то что получили
|
||||
if (streamingContent.trim()) {
|
||||
const partialMessage: ChatMessage = {
|
||||
id: generateId(),
|
||||
role: "assistant",
|
||||
content: streamingContent,
|
||||
timestamp: new Date(),
|
||||
};
|
||||
const partialSession: GameSession = {
|
||||
...session,
|
||||
messages: [...updatedMessages, partialMessage],
|
||||
};
|
||||
await apiSaveSession(story.id, partialSession);
|
||||
setSession(partialSession);
|
||||
}
|
||||
} else {
|
||||
setError(err instanceof Error ? err.message : "Произошла ошибка");
|
||||
setSession(session);
|
||||
setInput(userMessage.content);
|
||||
}
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
setStreamingContent("");
|
||||
abortControllerRef.current = null;
|
||||
inputRef.current?.focus();
|
||||
}
|
||||
};
|
||||
|
||||
const handleStop = () => {
|
||||
if (abortControllerRef.current) {
|
||||
abortControllerRef.current.abort();
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (e.key === "Enter" && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
@@ -369,7 +404,15 @@ export default function GamePage() {
|
||||
</div>
|
||||
))}
|
||||
|
||||
{isLoading && (
|
||||
{isLoading && streamingContent && (
|
||||
<div className="message assistant streaming">
|
||||
<div className="message-content">
|
||||
<ReactMarkdown>{streamingContent}</ReactMarkdown>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isLoading && !streamingContent && (
|
||||
<div className="message assistant loading">
|
||||
<div className="typing-indicator">
|
||||
<span></span>
|
||||
@@ -416,13 +459,19 @@ export default function GamePage() {
|
||||
disabled={isLoading}
|
||||
rows={2}
|
||||
/>
|
||||
<button
|
||||
onClick={handleSend}
|
||||
disabled={!input.trim() || isLoading}
|
||||
className="send-btn"
|
||||
>
|
||||
{isLoading ? "⏳" : "➤"}
|
||||
</button>
|
||||
{isLoading ? (
|
||||
<button onClick={handleStop} className="send-btn stop-btn">
|
||||
⏹
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
onClick={handleSend}
|
||||
disabled={!input.trim()}
|
||||
className="send-btn"
|
||||
>
|
||||
➤
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user