Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

URL based frontend state: React router #236

Merged
merged 29 commits into from
Apr 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
6443aa7
add react-router
mkorpela Mar 17, 2024
c89251a
extract state to App.tsx
mkorpela Mar 17, 2024
e702499
Chat.tsx: working
mkorpela Mar 17, 2024
9fe758c
assistant confs
mkorpela Mar 17, 2024
170ca4e
remove thread/new
mkorpela Mar 17, 2024
0ec49d2
update useEffect deps
mkorpela Mar 18, 2024
7c83235
config without shared_id
mkorpela Mar 18, 2024
9dcb2f3
sharing
mkorpela Mar 18, 2024
bbbd4ea
fe: refactor
mkorpela Mar 18, 2024
a8cbb55
matchingConfig
mkorpela Mar 18, 2024
34d90c0
cookie
mkorpela Mar 18, 2024
ba0f9c3
remove old mechanism
mkorpela Mar 18, 2024
da9ba0d
show all chats
mkorpela Mar 18, 2024
4f195cd
add react query
mkorpela Mar 20, 2024
a324ed8
isDocumentRetrieval: move useEffect to TypingBox
mkorpela Mar 20, 2024
3c6ee47
query
mkorpela Mar 20, 2024
fb74091
get_assistant interface returning also public assistants
mkorpela Mar 20, 2024
076010f
Chat.tsx: use react query
mkorpela Mar 21, 2024
5ce9458
TypingBox: remove dependency to configs
mkorpela Mar 21, 2024
54cb265
useThreadAndAssistant
mkorpela Mar 21, 2024
c6596d6
working
mkorpela Mar 21, 2024
c484e43
fix lint
mkorpela Mar 21, 2024
8d2cc64
fix type
mkorpela Mar 21, 2024
1774627
rmeove unused import
mkorpela Mar 21, 2024
25e5300
Merge remote-tracking branch 'upstream/main' into topic-react-router
mkorpela Apr 2, 2024
241b93c
Routes to the top
mkorpela Apr 2, 2024
5f881b6
focus items on lists
mkorpela Apr 2, 2024
00c895f
tune loading a bit
mkorpela Apr 2, 2024
78ec2a2
more unique key for message
mkorpela Apr 2, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-dropzone": "^14.2.3",
"react-query": "^3.39.3",
"react-router-dom": "^6.22.3",
"tailwind-merge": "^2.0.0",
"uuid": "^9.0.1"
},
Expand Down
164 changes: 62 additions & 102 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,58 +1,37 @@
import { useCallback, useEffect, useMemo, useState } from "react";
import { useCallback, useState } from "react";
import { InformationCircleIcon } from "@heroicons/react/24/outline";
import { Chat } from "./components/Chat";
import { ChatList } from "./components/ChatList";
import { Layout } from "./components/Layout";
import { NewChat } from "./components/NewChat";
import { Chat as ChatType, useChatList } from "./hooks/useChatList";
import { useChatList } from "./hooks/useChatList";
import { useSchemas } from "./hooks/useSchemas";
import { useStreamState } from "./hooks/useStreamState";
import { useConfigList } from "./hooks/useConfigList";
import {
useConfigList,
Config as ConfigInterface,
} from "./hooks/useConfigList";
import { Config } from "./components/Config";
import { MessageWithFiles } from "./utils/formTypes.ts";
import { TYPE_NAME } from "./constants.ts";
import { useNavigate } from "react-router-dom";
import { useThreadAndAssistant } from "./hooks/useThreadAndAssistant.ts";

function App() {
const navigate = useNavigate();
const [sidebarOpen, setSidebarOpen] = useState(false);
const { configSchema, configDefaults } = useSchemas();
const { chats, currentChat, createChat, enterChat } = useChatList();
const { configs, currentConfig, saveConfig, enterConfig } = useConfigList();
const { chats, createChat } = useChatList();
const { configs, saveConfig } = useConfigList();
const { startStream, stopStream, stream } = useStreamState();
const [isDocumentRetrievalActive, setIsDocumentRetrievalActive] =
useState(false);
const { configSchema, configDefaults } = useSchemas();

useEffect(() => {
let configurable = null;
if (currentConfig) {
configurable = currentConfig?.config?.configurable;
}
if (currentChat && configs) {
const conf = configs.find(
(c) => c.assistant_id === currentChat.assistant_id,
);
configurable = conf?.config?.configurable;
}
const agent_type = configurable?.["type"] as TYPE_NAME | null;
if (agent_type === null || agent_type === "chatbot") {
setIsDocumentRetrievalActive(false);
return;
}
if (agent_type === "chat_retrieval") {
setIsDocumentRetrievalActive(true);
return;
}
const tools =
(configurable?.["type==agent/tools"] as { name: string }[]) ?? [];
setIsDocumentRetrievalActive(tools.some((t) => t.name === "Retrieval"));
}, [currentConfig, currentChat, configs]);
const { currentChat, assistantConfig, isLoading } = useThreadAndAssistant();

const startTurn = useCallback(
async (message?: MessageWithFiles, chat: ChatType | null = currentChat) => {
if (!chat) return;
const config = configs?.find(
(c) => c.assistant_id === chat.assistant_id,
)?.config;
if (!config) return;
async (
message: MessageWithFiles | null,
thread_id: string,
assistant_id: string,
) => {
const files = message?.files || [];
if (files.length > 0) {
const formData = files.reduce((formData, file) => {
Expand All @@ -61,7 +40,7 @@ function App() {
}, new FormData());
formData.append(
"config",
JSON.stringify({ configurable: { thread_id: chat.thread_id } }),
JSON.stringify({ configurable: { thread_id } }),
);
await fetch(`/ingest`, {
method: "POST",
Expand All @@ -79,93 +58,58 @@ function App() {
},
]
: null,
chat.assistant_id,
chat.thread_id,
assistant_id,
thread_id,
);
},
[currentChat, startStream, configs],
[startStream],
);

const startChat = useCallback(
async (message: MessageWithFiles) => {
if (!currentConfig) return;
const chat = await createChat(
message.message,
currentConfig.assistant_id,
);
return startTurn(message, chat);
async (config: ConfigInterface, message: MessageWithFiles) => {
const chat = await createChat(message.message, config.assistant_id);
navigate(`/thread/${chat.thread_id}`);
return startTurn(message, chat.thread_id, chat.assistant_id);
},
[createChat, startTurn, currentConfig],
[createChat, navigate, startTurn],
);

const selectChat = useCallback(
async (id: string | null) => {
if (currentChat) {
stopStream?.(true);
}
enterChat(id);
if (!id) {
enterConfig(configs?.[0]?.assistant_id ?? null);
const firstAssistant = configs?.[0]?.assistant_id ?? null;
navigate(firstAssistant ? `/assistant/${firstAssistant}` : "/");
window.scrollTo({ top: 0 });
} else {
navigate(`/thread/${id}`);
}
if (sidebarOpen) {
setSidebarOpen(false);
}
},
[enterChat, stopStream, sidebarOpen, currentChat, enterConfig, configs],
[currentChat, sidebarOpen, stopStream, configs, navigate],
);

const selectConfig = useCallback(
(id: string | null) => {
enterConfig(id);
enterChat(null);
navigate(id ? `/assistant/${id}` : "/");
},
[enterConfig, enterChat],
);

const content = currentChat ? (
<Chat
chat={currentChat}
startStream={startTurn}
stopStream={stopStream}
stream={stream}
isDocumentRetrievalActive={isDocumentRetrievalActive}
/>
) : currentConfig ? (
<NewChat
startChat={startChat}
configSchema={configSchema}
configDefaults={configDefaults}
configs={configs}
currentConfig={currentConfig}
saveConfig={saveConfig}
enterConfig={selectConfig}
isDocumentRetrievalActive={isDocumentRetrievalActive}
/>
) : (
<Config
className="mb-6"
config={currentConfig}
configSchema={configSchema}
configDefaults={configDefaults}
saveConfig={saveConfig}
/>
);

const currentChatConfig = configs?.find(
(c) => c.assistant_id === currentChat?.assistant_id,
[navigate],
);

return (
<Layout
subtitle={
currentChatConfig ? (
assistantConfig ? (
<span className="inline-flex gap-1 items-center">
{currentChatConfig.name}
{assistantConfig.name}
<InformationCircleIcon
className="h-5 w-5 cursor-pointer text-indigo-600"
onClick={() => {
selectConfig(currentChatConfig.assistant_id);
selectConfig(assistantConfig.assistant_id);
}}
/>
</span>
Expand All @@ -175,20 +119,36 @@ function App() {
setSidebarOpen={setSidebarOpen}
sidebar={
<ChatList
chats={useMemo(() => {
if (configs === null || chats === null) return null;
return chats.filter((c) =>
configs.some((config) => config.assistant_id === c.assistant_id),
);
}, [chats, configs])}
currentChat={currentChat}
chats={chats}
enterChat={selectChat}
currentConfig={currentConfig}
enterConfig={selectConfig}
/>
}
>
{configSchema ? content : null}
{currentChat && assistantConfig && (
<Chat startStream={startTurn} stopStream={stopStream} stream={stream} />
)}
{!currentChat && assistantConfig && (
<NewChat
startChat={startChat}
configSchema={configSchema}
configDefaults={configDefaults}
configs={configs}
saveConfig={saveConfig}
enterConfig={selectConfig}
/>
)}
{!currentChat && !assistantConfig && !isLoading && (
<Config
className="mb-6"
config={null}
configSchema={configSchema}
configDefaults={configDefaults}
saveConfig={saveConfig}
enterConfig={selectConfig}
/>
)}
{isLoading && <div>Loading...</div>}
</Layout>
);
}
Expand Down
16 changes: 16 additions & 0 deletions frontend/src/api/assistants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Config } from "../hooks/useConfigList";

export async function getAssistant(
assistantId: string,
): Promise<Config | null> {
try {
const response = await fetch(`/assistants/${assistantId}`);
if (!response.ok) {
return null;
}
return (await response.json()) as Config;
} catch (error) {
console.error("Failed to fetch assistant:", error);
return null;
}
}
14 changes: 14 additions & 0 deletions frontend/src/api/threads.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Chat } from "../hooks/useChatList.ts";

export async function getThread(threadId: string): Promise<Chat | null> {
try {
const response = await fetch(`/threads/${threadId}`);
if (!response.ok) {
return null;
}
return (await response.json()) as Chat;
} catch (error) {
console.error("Failed to fetch assistant:", error);
return null;
}
}
42 changes: 33 additions & 9 deletions frontend/src/components/Chat.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
import { useEffect, useRef } from "react";
import { Chat as ChatType } from "../hooks/useChatList";
import { StreamStateProps } from "../hooks/useStreamState";
import { useChatMessages } from "../hooks/useChatMessages";
import TypingBox from "./TypingBox";
import { Message } from "./Message";
import { ArrowDownCircleIcon } from "@heroicons/react/24/outline";
import { MessageWithFiles } from "../utils/formTypes.ts";
import { useParams } from "react-router-dom";
import { useThreadAndAssistant } from "../hooks/useThreadAndAssistant.ts";

interface ChatProps extends Pick<StreamStateProps, "stream" | "stopStream"> {
chat: ChatType;
startStream: (message?: MessageWithFiles) => Promise<void>;
isDocumentRetrievalActive: boolean;
startStream: (
message: MessageWithFiles | null,
thread_id: string,
assistant_id: string,
) => Promise<void>;
}

function usePrevious<T>(value: T): T | undefined {
Expand All @@ -22,11 +25,15 @@ function usePrevious<T>(value: T): T | undefined {
}

export function Chat(props: ChatProps) {
const { chatId } = useParams();
const { messages, resumeable } = useChatMessages(
props.chat.thread_id,
chatId ?? null,
props.stream,
props.stopStream,
);

const { currentChat, assistantConfig, isLoading } = useThreadAndAssistant();

const prevMessages = usePrevious(messages);
useEffect(() => {
scrollTo({
Expand All @@ -38,12 +45,16 @@ export function Chat(props: ChatProps) {
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [messages]);

if (isLoading) return <div>Loading...</div>;
if (!currentChat || !assistantConfig) return <div>No data.</div>;

return (
<div className="flex-1 flex flex-col items-stretch pb-[76px] pt-2">
{messages?.map((msg, i) => (
<Message
{...msg}
key={i}
key={`message-${i}`}
runId={
i === messages.length - 1 && props.stream?.status === "done"
? props.stream?.run_id
Expand All @@ -64,20 +75,33 @@ export function Chat(props: ChatProps) {
{resumeable && props.stream?.status !== "inflight" && (
<div
className="flex items-center rounded-md bg-blue-50 px-2 py-1 text-xs font-medium text-blue-800 ring-1 ring-inset ring-yellow-600/20 cursor-pointer"
onClick={() => props.startStream()}
onClick={() =>
props.startStream(
null,
currentChat.thread_id,
currentChat.assistant_id,
)
}
>
<ArrowDownCircleIcon className="h-5 w-5 mr-1" />
Click to continue.
</div>
)}
<div className="fixed left-0 lg:left-72 bottom-0 right-0 p-4">
<TypingBox
onSubmit={props.startStream}
onSubmit={(msg) =>
props.startStream(
msg,
currentChat.thread_id,
currentChat.assistant_id,
)
}
onInterrupt={
props.stream?.status === "inflight" ? props.stopStream : undefined
}
inflight={props.stream?.status === "inflight"}
isDocumentRetrievalActive={props.isDocumentRetrievalActive}
currentConfig={assistantConfig}
currentChat={currentChat}
/>
</div>
</div>
Expand Down
Loading
Loading