Files
NanoComic/frontend/src/App.tsx
zihanjian 8d4223fbdb refactor(agent): simplify web research and citation handling
feat(frontend): enhance image generation with e
2025-12-02 17:13:07 +08:00

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>
);
}