diff --git a/app/src/assets/generation.less b/app/src/assets/generation.less
index d9cc484b..224984c6 100644
--- a/app/src/assets/generation.less
+++ b/app/src/assets/generation.less
@@ -148,7 +148,6 @@
display: flex;
flex-direction: row;
width: 80%;
- gap: 8px;
margin: 0 auto;
max-width: 680px;
@@ -160,6 +159,7 @@
border-radius: var(--radius);
border: 1px solid hsl(var(--border-hover));
letter-spacing: 1px;
+ margin-right: 8px;
}
.action {
diff --git a/app/src/assets/home.less b/app/src/assets/home.less
index 78a071ea..cfdfd8ad 100644
--- a/app/src/assets/home.less
+++ b/app/src/assets/home.less
@@ -274,12 +274,12 @@
align-items: center;
flex-wrap: nowrap;
width: 100%;
- gap: 4px;
height: min-content;
.chat-box {
position: relative;
flex-grow: 1;
+ margin: 0 4px;
}
.input-box {
diff --git a/app/src/components/home/ChatInterface.tsx b/app/src/components/home/ChatInterface.tsx
new file mode 100644
index 00000000..4eaeeeb8
--- /dev/null
+++ b/app/src/components/home/ChatInterface.tsx
@@ -0,0 +1,77 @@
+import {useEffect, useRef, useState} from "react";
+import {Message} from "../../conversation/types.ts";
+import {useSelector} from "react-redux";
+import {selectCurrent, selectMessages} from "../../store/chat.ts";
+import {Button} from "../ui/button.tsx";
+import {ChevronDown} from "lucide-react";
+import MessageSegment from "../Message.tsx";
+import {connectionEvent} from "../../events/connection.ts";
+
+function ChatInterface() {
+ const ref = useRef(null);
+ const [scroll, setScroll] = useState(false);
+ const messages: Message[] = useSelector(selectMessages);
+ const current: number = useSelector(selectCurrent);
+
+ function listenScrolling() {
+ if (!ref.current) return;
+ const el = ref.current as HTMLDivElement;
+ const offset = el.scrollHeight - el.scrollTop - el.clientHeight;
+ setScroll(offset > 100);
+ }
+
+ useEffect(
+ function () {
+ if (!ref.current) return;
+ const el = ref.current as HTMLDivElement;
+ el.scrollTop = el.scrollHeight;
+ listenScrolling();
+ },
+ [messages],
+ );
+
+ useEffect(() => {
+ if (!ref.current) return;
+ const el = ref.current as HTMLDivElement;
+ el.addEventListener("scroll", listenScrolling);
+ }, [ref]);
+
+ return (
+ <>
+
+
+
+
+
+ {messages.map((message, i) => (
+
{
+ connectionEvent.emit({
+ id: current,
+ event: e,
+ });
+ }}
+ key={i}
+ />
+ ))}
+
+ >
+ );
+}
+
+export default ChatInterface;
diff --git a/app/src/components/home/ChatWrapper.tsx b/app/src/components/home/ChatWrapper.tsx
new file mode 100644
index 00000000..6c7e7bcc
--- /dev/null
+++ b/app/src/components/home/ChatWrapper.tsx
@@ -0,0 +1,172 @@
+import {useTranslation} from "react-i18next";
+import React, {useEffect, useRef, useState} from "react";
+import FileProvider, {FileObject} from "../FileProvider.tsx";
+import {useDispatch, useSelector} from "react-redux";
+import {selectAuthenticated, selectInit} from "../../store/auth.ts";
+import {selectMessages, selectModel, selectWeb, setWeb} from "../../store/chat.ts";
+import {manager} from "../../conversation/manager.ts";
+import {formatMessage} from "../../utils.ts";
+import ChatInterface from "./ChatInterface.tsx";
+import {Button} from "../ui/button.tsx";
+import router from "../../router.ts";
+import {ChevronRight, FolderKanban, Globe} from "lucide-react";
+import {Tooltip, TooltipContent, TooltipProvider, TooltipTrigger} from "../ui/tooltip.tsx";
+import {Toggle} from "../ui/toggle.tsx";
+import {Input} from "../ui/input.tsx";
+import EditorProvider from "../EditorProvider.tsx";
+import ModelSelector from "./ModelSelector.tsx";
+
+function ChatWrapper() {
+ const { t } = useTranslation();
+ const [file, setFile] = useState({
+ name: "",
+ content: "",
+ });
+ const [clearEvent, setClearEvent] = useState<() => void>(() => {});
+ const [input, setInput] = useState("");
+ const dispatch = useDispatch();
+ const init = useSelector(selectInit);
+ const auth = useSelector(selectAuthenticated);
+ const model = useSelector(selectModel);
+ const web = useSelector(selectWeb);
+ const messages = useSelector(selectMessages);
+ const target = useRef(null);
+ manager.setDispatch(dispatch);
+
+ function clearFile() {
+ clearEvent?.();
+ }
+
+ async function processSend(
+ data: string,
+ auth: boolean,
+ model: string,
+ web: boolean,
+ ): Promise {
+ const message: string = formatMessage(file, data);
+ if (message.length > 0 && data.trim().length > 0) {
+ if (await manager.send(t, auth, { message, web, model, type: "chat" })) {
+ clearFile();
+ return true;
+ }
+ }
+ return false;
+ }
+
+ async function handleSend(auth: boolean, model: string, web: boolean) {
+ // because of the function wrapper, we need to update the selector state using props.
+ if (await processSend(input, auth, model, web)) {
+ setInput("");
+ }
+ }
+
+ window.addEventListener("load", () => {
+ const el = document.getElementById("input");
+ if (el) el.focus();
+ });
+
+ useEffect(() => {
+ if (!init) return;
+ const search = new URLSearchParams(window.location.search);
+ const query = (search.get("q") || "").trim();
+ if (query.length > 0) processSend(query, auth, model, web).then();
+ window.history.replaceState({}, "", "/");
+ }, [init]);
+
+ return (
+
+
+ {messages.length > 0 ? (
+
+ ) : (
+
+
+
+ )}
+
+
+
+
+
+
+ dispatch(setWeb(state))
+ }
+ variant={`outline`}
+ >
+
+
+
+
+ {t("chat.web")}
+
+
+
+
+ {auth && (
+
+ )}
+ ) =>
+ setInput(e.target.value)
+ }
+ placeholder={t("chat.placeholder")}
+ onKeyDown={async (e: React.KeyboardEvent) => {
+ if (e.key === "Enter") await handleSend(auth, model, web);
+ }}
+ />
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+export default ChatWrapper;
diff --git a/app/src/components/home/SideBar.tsx b/app/src/components/home/SideBar.tsx
new file mode 100644
index 00000000..b374c355
--- /dev/null
+++ b/app/src/components/home/SideBar.tsx
@@ -0,0 +1,259 @@
+import {useTranslation} from "react-i18next";
+import {useDispatch, useSelector} from "react-redux";
+import {RootState} from "../../store";
+import {selectAuthenticated} from "../../store/auth.ts";
+import {selectCurrent, selectHistory} from "../../store/chat.ts";
+import {useRef, useState} from "react";
+import {ConversationInstance} from "../../conversation/types.ts";
+import {useToast} from "../ui/use-toast.ts";
+import {copyClipboard, extractMessage, filterMessage, mobile, useAnimation, useEffectAsync} from "../../utils.ts";
+import {deleteConversation, toggleConversation, updateConversationList} from "../../conversation/history.ts";
+import {Button} from "../ui/button.tsx";
+import {setMenu} from "../../store/menu.ts";
+import {Copy, LogIn, Plus, RotateCw} from "lucide-react";
+import ConversationSegment from "./ConversationSegment.tsx";
+import {
+ AlertDialog, AlertDialogAction, AlertDialogCancel,
+ AlertDialogContent,
+ AlertDialogDescription, AlertDialogFooter,
+ AlertDialogHeader,
+ AlertDialogTitle
+} from "../ui/alert-dialog.tsx";
+import {shareConversation} from "../../conversation/sharing.ts";
+import {Input} from "../ui/input.tsx";
+import {login} from "../../conf.ts";
+
+function SideBar() {
+ const { t } = useTranslation();
+ const dispatch = useDispatch();
+ const open = useSelector((state: RootState) => state.menu.open);
+ const auth = useSelector(selectAuthenticated);
+ const current = useSelector(selectCurrent);
+ const [operateConversation, setOperateConversation] = useState<{
+ target: ConversationInstance | null;
+ type: string;
+ }>({ target: null, type: "" });
+ const { toast } = useToast();
+ const history: ConversationInstance[] = useSelector(selectHistory);
+ const refresh = useRef(null);
+ const [shared, setShared] = useState("");
+ useEffectAsync(async () => {
+ await updateConversationList(dispatch);
+ }, []);
+
+ // @ts-ignore
+ return (
+
+ {auth ? (
+
+
+
+
+
+
+
+ {history.length ? (
+ history.map((conversation, i) => (
+
+ ))
+ ) : (
+
{t("conversation.empty")}
+ )}
+
+
{
+ if (!open) setOperateConversation({ target: null, type: "" });
+ }}
+ >
+
+
+
+ {t("conversation.remove-title")}
+
+
+ {t("conversation.remove-description")}
+
+ {extractMessage(
+ filterMessage(operateConversation?.target?.name || ""),
+ )}
+
+ {t("end")}
+
+
+
+
+ {t("conversation.cancel")}
+
+ {
+ e.preventDefault();
+ e.stopPropagation();
+
+ if (
+ await deleteConversation(
+ dispatch,
+ operateConversation?.target?.id || -1,
+ )
+ )
+ toast({
+ title: t("conversation.delete-success"),
+ description: t("conversation.delete-success-prompt"),
+ });
+ else
+ toast({
+ title: t("conversation.delete-failed"),
+ description: t("conversation.delete-failed-prompt"),
+ });
+ setOperateConversation({ target: null, type: "" });
+ }}
+ >
+ {t("conversation.delete")}
+
+
+
+
+
+
{
+ if (!open) setOperateConversation({ target: null, type: "" });
+ }}
+ >
+
+
+ {t("share.title")}
+
+ {t("share.description")}
+
+ {extractMessage(
+ filterMessage(operateConversation?.target?.name || ""),
+ )}
+
+ {t("end")}
+
+
+
+
+ {t("conversation.cancel")}
+
+ {
+ e.preventDefault();
+ e.stopPropagation();
+
+ const resp = await shareConversation(
+ operateConversation?.target?.id || -1,
+ );
+ if (resp.status)
+ setShared(`${location.origin}/share/${resp.data}`);
+ else
+ toast({
+ title: t("share.failed"),
+ description: resp.message,
+ });
+
+ setOperateConversation({ target: null, type: "" });
+ }}
+ >
+ {t("share.title")}
+
+
+
+
+
+
0}
+ onOpenChange={(open) => {
+ if (!open) {
+ setShared("");
+ setOperateConversation({ target: null, type: "" });
+ }
+ }}
+ >
+
+
+ {t("share.success")}
+
+
+
+
+
+
+
+
+ {t("close")}
+ {
+ e.preventDefault();
+ e.stopPropagation();
+ window.open(shared, "_blank");
+ }}
+ >
+ {t("share.view")}
+
+
+
+
+
+ ) : (
+
+ )}
+
+ );
+}
+
+export default SideBar;
diff --git a/app/src/routes/Home.tsx b/app/src/routes/Home.tsx
index 674e77ae..3ec682fa 100644
--- a/app/src/routes/Home.tsx
+++ b/app/src/routes/Home.tsx
@@ -1,527 +1,7 @@
import "../assets/home.less";
import "../assets/chat.less";
-import { Input } from "../components/ui/input.tsx";
-import { Toggle } from "../components/ui/toggle.tsx";
-import {
- ChevronDown,
- ChevronRight,
- Copy,
- FolderKanban,
- Globe,
- LogIn,
- Plus,
- RotateCw,
-} from "lucide-react";
-import { Button } from "../components/ui/button.tsx";
-import {
- Tooltip,
- TooltipContent,
- TooltipProvider,
- TooltipTrigger,
-} from "../components/ui/tooltip.tsx";
-import { useDispatch, useSelector } from "react-redux";
-import type { RootState } from "../store";
-import { selectAuthenticated, selectInit } from "../store/auth.ts";
-import { login } from "../conf.ts";
-import {
- deleteConversation,
- toggleConversation,
- updateConversationList,
-} from "../conversation/history.ts";
-import { shareConversation } from "../conversation/sharing.ts";
-import React, { useEffect, useRef, useState } from "react";
-import {
- filterMessage,
- extractMessage,
- formatMessage,
- mobile,
- useAnimation,
- useEffectAsync,
- copyClipboard,
-} from "../utils.ts";
-import { useToast } from "../components/ui/use-toast.ts";
-import { ConversationInstance, Message } from "../conversation/types.ts";
-import {
- selectCurrent,
- selectModel,
- selectHistory,
- selectMessages,
- selectWeb,
- setWeb,
-} from "../store/chat.ts";
-import {
- AlertDialog,
- AlertDialogAction,
- AlertDialogCancel,
- AlertDialogContent,
- AlertDialogDescription,
- AlertDialogFooter,
- AlertDialogHeader,
- AlertDialogTitle,
-} from "../components/ui/alert-dialog.tsx";
-import { manager } from "../conversation/manager.ts";
-import { useTranslation } from "react-i18next";
-import MessageSegment from "../components/Message.tsx";
-import { setMenu } from "../store/menu.ts";
-import FileProvider, { FileObject } from "../components/FileProvider.tsx";
-import router from "../router.ts";
-import EditorProvider from "../components/EditorProvider.tsx";
-import ConversationSegment from "../components/home/ConversationSegment.tsx";
-import { connectionEvent } from "../events/connection.ts";
-import ModelSelector from "../components/home/ModelSelector.tsx";
-
-function SideBar() {
- const { t } = useTranslation();
- const dispatch = useDispatch();
- const open = useSelector((state: RootState) => state.menu.open);
- const auth = useSelector(selectAuthenticated);
- const current = useSelector(selectCurrent);
- const [operateConversation, setOperateConversation] = useState<{
- target: ConversationInstance | null;
- type: string;
- }>({ target: null, type: "" });
- const { toast } = useToast();
- const history: ConversationInstance[] = useSelector(selectHistory);
- const refresh = useRef(null);
- const [shared, setShared] = useState("");
- useEffectAsync(async () => {
- await updateConversationList(dispatch);
- }, []);
-
- // @ts-ignore
- return (
-
- {auth ? (
-
-
-
-
-
-
-
- {history.length ? (
- history.map((conversation, i) => (
-
- ))
- ) : (
-
{t("conversation.empty")}
- )}
-
-
{
- if (!open) setOperateConversation({ target: null, type: "" });
- }}
- >
-
-
-
- {t("conversation.remove-title")}
-
-
- {t("conversation.remove-description")}
-
- {extractMessage(
- filterMessage(operateConversation?.target?.name || ""),
- )}
-
- {t("end")}
-
-
-
-
- {t("conversation.cancel")}
-
- {
- e.preventDefault();
- e.stopPropagation();
-
- if (
- await deleteConversation(
- dispatch,
- operateConversation?.target?.id || -1,
- )
- )
- toast({
- title: t("conversation.delete-success"),
- description: t("conversation.delete-success-prompt"),
- });
- else
- toast({
- title: t("conversation.delete-failed"),
- description: t("conversation.delete-failed-prompt"),
- });
- setOperateConversation({ target: null, type: "" });
- }}
- >
- {t("conversation.delete")}
-
-
-
-
-
-
{
- if (!open) setOperateConversation({ target: null, type: "" });
- }}
- >
-
-
- {t("share.title")}
-
- {t("share.description")}
-
- {extractMessage(
- filterMessage(operateConversation?.target?.name || ""),
- )}
-
- {t("end")}
-
-
-
-
- {t("conversation.cancel")}
-
- {
- e.preventDefault();
- e.stopPropagation();
-
- const resp = await shareConversation(
- operateConversation?.target?.id || -1,
- );
- if (resp.status)
- setShared(`${location.origin}/share/${resp.data}`);
- else
- toast({
- title: t("share.failed"),
- description: resp.message,
- });
-
- setOperateConversation({ target: null, type: "" });
- }}
- >
- {t("share.title")}
-
-
-
-
-
-
0}
- onOpenChange={(open) => {
- if (!open) {
- setShared("");
- setOperateConversation({ target: null, type: "" });
- }
- }}
- >
-
-
- {t("share.success")}
-
-
-
-
-
-
-
-
- {t("close")}
- {
- e.preventDefault();
- e.stopPropagation();
- window.open(shared, "_blank");
- }}
- >
- {t("share.view")}
-
-
-
-
-
- ) : (
-
- )}
-
- );
-}
-
-function ChatInterface() {
- const ref = useRef(null);
- const [scroll, setScroll] = useState(false);
- const messages: Message[] = useSelector(selectMessages);
- const current: number = useSelector(selectCurrent);
-
- function listenScrolling() {
- if (!ref.current) return;
- const el = ref.current as HTMLDivElement;
- const offset = el.scrollHeight - el.scrollTop - el.clientHeight;
- setScroll(offset > 100);
- }
-
- useEffect(
- function () {
- if (!ref.current) return;
- const el = ref.current as HTMLDivElement;
- el.scrollTop = el.scrollHeight;
- listenScrolling();
- },
- [messages],
- );
-
- useEffect(() => {
- if (!ref.current) return;
- const el = ref.current as HTMLDivElement;
- el.addEventListener("scroll", listenScrolling);
- }, [ref]);
-
- return (
- <>
-
-
-
-
-
- {messages.map((message, i) => (
-
{
- connectionEvent.emit({
- id: current,
- event: e,
- });
- }}
- key={i}
- />
- ))}
-
- >
- );
-}
-
-function ChatWrapper() {
- const { t } = useTranslation();
- const [file, setFile] = useState({
- name: "",
- content: "",
- });
- const [clearEvent, setClearEvent] = useState<() => void>(() => {});
- const [input, setInput] = useState("");
- const dispatch = useDispatch();
- const init = useSelector(selectInit);
- const auth = useSelector(selectAuthenticated);
- const model = useSelector(selectModel);
- const web = useSelector(selectWeb);
- const messages = useSelector(selectMessages);
- const target = useRef(null);
- manager.setDispatch(dispatch);
-
- function clearFile() {
- clearEvent?.();
- }
-
- async function processSend(
- data: string,
- auth: boolean,
- model: string,
- web: boolean,
- ): Promise {
- const message: string = formatMessage(file, data);
- if (message.length > 0 && data.trim().length > 0) {
- if (await manager.send(t, auth, { message, web, model, type: "chat" })) {
- clearFile();
- return true;
- }
- }
- return false;
- }
-
- async function handleSend(auth: boolean, model: string, web: boolean) {
- // because of the function wrapper, we need to update the selector state using props.
- if (await processSend(input, auth, model, web)) {
- setInput("");
- }
- }
-
- window.addEventListener("load", () => {
- const el = document.getElementById("input");
- if (el) el.focus();
- });
-
- useEffect(() => {
- if (!init) return;
- const search = new URLSearchParams(window.location.search);
- const query = (search.get("q") || "").trim();
- if (query.length > 0) processSend(query, auth, model, web).then();
- window.history.replaceState({}, "", "/");
- }, [init]);
-
- return (
-
-
- {messages.length > 0 ? (
-
- ) : (
-
-
-
- )}
-
-
-
-
-
-
- dispatch(setWeb(state))
- }
- variant={`outline`}
- >
-
-
-
-
- {t("chat.web")}
-
-
-
-
- {auth && (
-
- )}
- ) =>
- setInput(e.target.value)
- }
- placeholder={t("chat.placeholder")}
- onKeyDown={async (e: React.KeyboardEvent) => {
- if (e.key === "Enter") await handleSend(auth, model, web);
- }}
- />
-
-
-
-
-
-
-
-
-
-
- );
-}
+import ChatWrapper from "../components/home/ChatWrapper.tsx";
+import SideBar from "../components/home/SideBar.tsx";
function Home() {
return (