-
Notifications
You must be signed in to change notification settings - Fork 1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
browser adapter: fix gap in flexbox (chronium <84)
- Loading branch information
1 parent
663fcf8
commit cb5829b
Showing
6 changed files
with
512 additions
and
524 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 ( | ||
<> | ||
<div className={`chat-content`} ref={ref}> | ||
<div className={`scroll-action ${scroll ? "active" : ""}`}> | ||
<Button | ||
variant={`outline`} | ||
size={`icon`} | ||
onClick={() => { | ||
if (!ref.current) return; | ||
const el = ref.current as HTMLDivElement; | ||
el.scrollTo({ | ||
top: el.scrollHeight, | ||
behavior: "smooth", | ||
}); | ||
}} | ||
> | ||
<ChevronDown className={`h-4 w-4`} /> | ||
</Button> | ||
</div> | ||
|
||
{messages.map((message, i) => ( | ||
<MessageSegment | ||
message={message} | ||
end={i === messages.length - 1} | ||
onEvent={(e: string) => { | ||
connectionEvent.emit({ | ||
id: current, | ||
event: e, | ||
}); | ||
}} | ||
key={i} | ||
/> | ||
))} | ||
</div> | ||
</> | ||
); | ||
} | ||
|
||
export default ChatInterface; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<FileObject>({ | ||
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<boolean> { | ||
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 ( | ||
<div className={`chat-container`}> | ||
<div className={`chat-wrapper`}> | ||
{messages.length > 0 ? ( | ||
<ChatInterface /> | ||
) : ( | ||
<div className={`chat-product`}> | ||
<Button | ||
variant={`outline`} | ||
onClick={() => router.navigate("/generate")} | ||
> | ||
<FolderKanban className={`h-4 w-4 mr-1.5`} /> | ||
{t("generate.title")} | ||
<ChevronRight className={`h-4 w-4 ml-2`} /> | ||
</Button> | ||
</div> | ||
)} | ||
<div className={`chat-input`}> | ||
<div className={`input-wrapper`}> | ||
<TooltipProvider> | ||
<Tooltip> | ||
<TooltipTrigger asChild> | ||
<Toggle | ||
aria-label={t("chat.web-aria")} | ||
defaultPressed={true} | ||
onPressedChange={(state: boolean) => | ||
dispatch(setWeb(state)) | ||
} | ||
variant={`outline`} | ||
> | ||
<Globe className={`h-4 w-4 web ${web ? "enable" : ""}`} /> | ||
</Toggle> | ||
</TooltipTrigger> | ||
<TooltipContent> | ||
<p className={`tooltip`}>{t("chat.web")}</p> | ||
</TooltipContent> | ||
</Tooltip> | ||
</TooltipProvider> | ||
<div className={`chat-box`}> | ||
{auth && ( | ||
<FileProvider | ||
id={`file`} | ||
className={`file`} | ||
onChange={setFile} | ||
maxLength={4000 * 1.25} | ||
setClearEvent={setClearEvent} | ||
/> | ||
)} | ||
<Input | ||
id={`input`} | ||
className={`input-box`} | ||
ref={target} | ||
value={input} | ||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => | ||
setInput(e.target.value) | ||
} | ||
placeholder={t("chat.placeholder")} | ||
onKeyDown={async (e: React.KeyboardEvent<HTMLInputElement>) => { | ||
if (e.key === "Enter") await handleSend(auth, model, web); | ||
}} | ||
/> | ||
<EditorProvider | ||
value={input} | ||
onChange={setInput} | ||
className={`editor`} | ||
id={`editor`} | ||
placeholder={t("chat.placeholder")} | ||
maxLength={8000} | ||
/> | ||
</div> | ||
<Button | ||
size={`icon`} | ||
variant="outline" | ||
className={`send-button`} | ||
onClick={() => handleSend(auth, model, web)} | ||
> | ||
<svg | ||
className="h-4 w-4" | ||
xmlns="http://www.w3.org/2000/svg" | ||
width="24" | ||
height="24" | ||
viewBox="0 0 24 24" | ||
> | ||
<path d="m21.426 11.095-17-8A1 1 0 0 0 3.03 4.242l1.212 4.849L12 12l-7.758 2.909-1.212 4.849a.998.998 0 0 0 1.396 1.147l17-8a1 1 0 0 0 0-1.81z"></path> | ||
</svg> | ||
</Button> | ||
</div> | ||
<div className={`input-options`}> | ||
<ModelSelector /> | ||
</div> | ||
</div> | ||
</div> | ||
</div> | ||
); | ||
} | ||
|
||
export default ChatWrapper; |
Oops, something went wrong.