mirror of
https://github.com/Zippland/NanoComic.git
synced 2026-01-19 01:21:08 +08:00
208 lines
6.6 KiB
TypeScript
208 lines
6.6 KiB
TypeScript
import { useStream } from "@langchain/langgraph-sdk/react";
|
|
import type { Message } from "@langchain/langgraph-sdk";
|
|
import { useState, useEffect, useRef, useCallback } from "react";
|
|
import { ProcessedEvent } from "@/components/ActivityTimeline";
|
|
import { WelcomeScreen } from "@/components/WelcomeScreen";
|
|
import { ChatMessagesView } from "@/components/ChatMessagesView";
|
|
import { Button } from "@/components/ui/button";
|
|
|
|
export default function App() {
|
|
const [processedEventsTimeline, setProcessedEventsTimeline] = useState<
|
|
ProcessedEvent[]
|
|
>([]);
|
|
const [historicalActivities, setHistoricalActivities] = useState<
|
|
Record<string, ProcessedEvent[]>
|
|
>({});
|
|
const [imageConfig, setImageConfig] = useState({
|
|
aspectRatio: "16:9",
|
|
imageSize: "1K",
|
|
});
|
|
const scrollAreaRef = useRef<HTMLDivElement>(null);
|
|
const hasFinalizeEventOccurredRef = useRef(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const thread = useStream<{
|
|
messages: Message[];
|
|
initial_search_query_count: number;
|
|
max_research_loops: number;
|
|
reasoning_model: string;
|
|
language: string;
|
|
}>({
|
|
apiUrl: import.meta.env.DEV
|
|
? "http://localhost:2024"
|
|
: "http://localhost:8123",
|
|
assistantId: "agent",
|
|
messagesKey: "messages",
|
|
onUpdateEvent: (event: any) => {
|
|
let processedEvent: ProcessedEvent | null = null;
|
|
if (event.generate_query) {
|
|
processedEvent = {
|
|
title: "Generating Search Queries",
|
|
data: event.generate_query?.search_query?.join(", ") || "",
|
|
};
|
|
} else if (event.web_research) {
|
|
const sources = event.web_research.sources_gathered || [];
|
|
const numSources = sources.length;
|
|
const uniqueLabels = [
|
|
...new Set(sources.map((s: any) => s.label).filter(Boolean)),
|
|
];
|
|
const exampleLabels = uniqueLabels.slice(0, 3).join(", ");
|
|
processedEvent = {
|
|
title: "Web Research",
|
|
data: `Gathered ${numSources} sources. Related to: ${
|
|
exampleLabels || "N/A"
|
|
}.`,
|
|
};
|
|
} else if (event.reflection) {
|
|
processedEvent = {
|
|
title: "Reflection",
|
|
data: "Analysing Web Research Results",
|
|
};
|
|
} else if (event.finalize_answer) {
|
|
processedEvent = {
|
|
title: "Generate Scripts",
|
|
data: "Composing and presenting the final answer.",
|
|
};
|
|
hasFinalizeEventOccurredRef.current = true;
|
|
}
|
|
if (processedEvent) {
|
|
setProcessedEventsTimeline((prevEvents) => [
|
|
...prevEvents,
|
|
processedEvent!,
|
|
]);
|
|
}
|
|
},
|
|
onError: (error: any) => {
|
|
setError(error.message);
|
|
},
|
|
});
|
|
|
|
useEffect(() => {
|
|
if (scrollAreaRef.current) {
|
|
const scrollViewport = scrollAreaRef.current.querySelector(
|
|
"[data-radix-scroll-area-viewport]"
|
|
);
|
|
if (scrollViewport) {
|
|
scrollViewport.scrollTop = scrollViewport.scrollHeight;
|
|
}
|
|
}
|
|
}, [thread.messages]);
|
|
|
|
useEffect(() => {
|
|
if (
|
|
hasFinalizeEventOccurredRef.current &&
|
|
!thread.isLoading &&
|
|
thread.messages.length > 0
|
|
) {
|
|
const lastMessage = thread.messages[thread.messages.length - 1];
|
|
if (lastMessage && lastMessage.type === "ai" && lastMessage.id) {
|
|
setHistoricalActivities((prev) => ({
|
|
...prev,
|
|
[lastMessage.id!]: [...processedEventsTimeline],
|
|
}));
|
|
}
|
|
hasFinalizeEventOccurredRef.current = false;
|
|
}
|
|
}, [thread.messages, thread.isLoading, processedEventsTimeline]);
|
|
|
|
const handleSubmit = useCallback(
|
|
(
|
|
submittedInputValue: string,
|
|
effort: string,
|
|
model: string,
|
|
language: string,
|
|
aspectRatio: string,
|
|
imageSize: string
|
|
) => {
|
|
if (!submittedInputValue.trim()) return;
|
|
setProcessedEventsTimeline([]);
|
|
hasFinalizeEventOccurredRef.current = false;
|
|
|
|
// convert effort to, initial_search_query_count and max_research_loops
|
|
// low means max 1 loop and 1 query
|
|
// medium means max 3 loops and 3 queries
|
|
// high means max 10 loops and 5 queries
|
|
let initial_search_query_count = 0;
|
|
let max_research_loops = 0;
|
|
switch (effort) {
|
|
case "low":
|
|
initial_search_query_count = 1;
|
|
max_research_loops = 1;
|
|
break;
|
|
case "medium":
|
|
initial_search_query_count = 3;
|
|
max_research_loops = 3;
|
|
break;
|
|
case "high":
|
|
initial_search_query_count = 5;
|
|
max_research_loops = 10;
|
|
break;
|
|
}
|
|
|
|
const newMessages: Message[] = [
|
|
...(thread.messages || []),
|
|
{
|
|
type: "human",
|
|
content: submittedInputValue,
|
|
id: Date.now().toString(),
|
|
},
|
|
];
|
|
setImageConfig({ aspectRatio, imageSize });
|
|
thread.submit({
|
|
messages: newMessages,
|
|
initial_search_query_count: initial_search_query_count,
|
|
max_research_loops: max_research_loops,
|
|
reasoning_model: model,
|
|
language,
|
|
aspect_ratio: aspectRatio,
|
|
image_size: imageSize,
|
|
});
|
|
},
|
|
[thread]
|
|
);
|
|
|
|
const handleCancel = useCallback(() => {
|
|
thread.stop();
|
|
window.location.reload();
|
|
}, [thread]);
|
|
|
|
return (
|
|
<div className="flex h-screen bg-neutral-800 text-neutral-100 font-sans antialiased">
|
|
<main className="h-full w-full max-w-4xl mx-auto">
|
|
{thread.messages.length === 0 ? (
|
|
<WelcomeScreen
|
|
handleSubmit={handleSubmit}
|
|
isLoading={thread.isLoading}
|
|
onCancel={handleCancel}
|
|
/>
|
|
) : error ? (
|
|
<div className="flex flex-col items-center justify-center h-full">
|
|
<div className="flex flex-col items-center justify-center gap-4">
|
|
<h1 className="text-2xl text-red-400 font-bold">Error</h1>
|
|
<p className="text-red-400">{JSON.stringify(error)}</p>
|
|
|
|
<Button
|
|
variant="destructive"
|
|
onClick={() => window.location.reload()}
|
|
>
|
|
Retry
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<ChatMessagesView
|
|
messages={thread.messages}
|
|
isLoading={thread.isLoading}
|
|
scrollAreaRef={scrollAreaRef}
|
|
onSubmit={handleSubmit}
|
|
onCancel={handleCancel}
|
|
liveActivityEvents={processedEventsTimeline}
|
|
historicalActivities={historicalActivities}
|
|
aspectRatio={imageConfig.aspectRatio}
|
|
imageSize={imageConfig.imageSize}
|
|
/>
|
|
)}
|
|
</main>
|
|
</div>
|
|
);
|
|
}
|