update scroll action and fix file style in mobile

This commit is contained in:
Zhang Minghan 2023-11-05 10:06:47 +08:00
parent 76235ca796
commit 5a20c5282e
21 changed files with 375 additions and 267 deletions

View File

@ -22,6 +22,7 @@
.file-wrapper {
margin-top: 24px !important;
padding: 8px 0;
max-width: calc(90vw - 3rem);
}
.drop-window {
@ -103,3 +104,11 @@
}
}
}
.file-name {
word-wrap: anywhere;
word-break: break-all;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}

View File

@ -13,6 +13,7 @@
border: 1px solid hsl(var(--border-hover));
background: hsl(var(--background-container)) !important;
border-radius: var(--radius);
white-space: pre-wrap;
svg {
width: 16px;

View File

@ -14,10 +14,11 @@
.scroll-action {
position: absolute;
z-index: 12;
bottom: 112px;
right: 36px;
opacity: 0;
right: 36px;
bottom: 12rem;
transition: .25s;
pointer-events: none;
button {
border-color: rgba(0,0,0,0) !important;
@ -25,6 +26,11 @@
&.active {
opacity: 0.8;
pointer-events: all;
}
@media (max-width: 668px) {
bottom: 8.5rem;
}
}

View File

@ -318,6 +318,7 @@
padding: 6px 24px;
.input-wrapper {
position: relative;
display: flex;
flex-direction: row;
align-items: center;

View File

@ -19,19 +19,11 @@ import { Button } from "./ui/button.tsx";
type RichEditorProps = {
value: string;
onChange: (value: string) => void;
className?: string;
id?: string;
placeholder?: string;
maxLength?: number;
};
function RichEditor({
value,
onChange,
id,
placeholder,
maxLength,
}: RichEditorProps) {
function RichEditor({ value, onChange, maxLength }: RichEditorProps) {
const { t } = useTranslation();
const input = useRef(null);
const [openPreview, setOpenPreview] = useState(!mobile);
const [openInput, setOpenInput] = useState(true);
@ -117,10 +109,10 @@ function RichEditor({
>
{openInput && (
<Textarea
placeholder={placeholder}
placeholder={t("chat.placeholder")}
value={value}
className={`editor-input`}
id={id}
id={`editor`}
maxLength={maxLength}
onChange={(e) => onChange(e.target.value)}
ref={input}
@ -142,7 +134,7 @@ function EditorProvider(props: RichEditorProps) {
<>
<Dialog>
<DialogTrigger asChild>
<div className={`editor-action active ${props.className}`}>
<div className={`editor-action active editor`}>
<Maximize className={`h-3.5 w-3.5`} />
</div>
</DialogTrigger>

View File

@ -31,14 +31,11 @@ const MaxFileSize = 1024 * 1024 * 25; // 25MB File Size Limit
const MaxPromptSize = 5000; // 5000 Prompt Size Limit (to avoid token overflow)
type FileProviderProps = {
id: string;
className?: string;
value: FileArray;
onChange?: (value: FileArray) => void;
};
function FileProvider({ id, className, value, onChange }: FileProviderProps) {
function FileProvider({ value, onChange }: FileProviderProps) {
const { t } = useTranslation();
const { toast } = useToast();
const model = useSelector(selectModel);
@ -82,7 +79,7 @@ function FileProvider({ id, className, value, onChange }: FileProviderProps) {
<AlertTitle>{t("file.type")}</AlertTitle>
</Alert>
<FileList value={value} removeFile={removeFile} />
<FileInput id={id} className={className} addFile={addFile} />
<FileInput id={"file"} className={"file"} addFile={addFile} />
</div>
</DialogDescription>
</DialogHeader>
@ -111,7 +108,7 @@ function FileList({ value, removeFile }: FileListProps) {
<div className={`file-list`}>
{value.length > 3 && full && (
<div className={`file-item`}>
<Paperclip className={`h-4 w-4 ml-2 mr-1.5`} />
<Paperclip className={`flex-shrink-0 h-4 w-4 ml-2 mr-1.5`} />
<div className={`file-name mr-1`}>
{t("file.number", { number: value.length })}
</div>
@ -129,7 +126,7 @@ function FileList({ value, removeFile }: FileListProps) {
{value.length <= 3 || full ? (
value.map((file, index) => (
<div className={`file-item`} key={index}>
<File className={`h-4 w-4 ml-2 mr-1.5`} />
<File className={`flex-shrink-0 h-4 w-4 ml-2 mr-1.5`} />
<div className={`file-name mr-1`}>{file.name}</div>
<div className={`grow`} />
<div className={`file-size mr-2`}>
@ -147,7 +144,7 @@ function FileList({ value, removeFile }: FileListProps) {
))
) : (
<div className={`file-item`}>
<Paperclip className={`h-4 w-4 ml-2 mr-1.5`} />
<Paperclip className={`flex-shrink-0 h-4 w-4 ml-2 mr-1.5`} />
<div className={`file-name mr-1`}>
{t("file.zipper", {
filename: file.name,

View File

@ -27,6 +27,7 @@ function ReloadPrompt() {
const before = getMemory("version");
if (before.length > 0 && before !== version) {
setMemory("version", version);
toast({
title: t("service.update-success"),
description: t("service.update-success-prompt"),

View File

@ -0,0 +1,53 @@
import { openDialog } from "@/store/settings.ts";
import { version } from "@/conf.ts";
import { useDispatch } from "react-redux";
function ChatFooter() {
const dispatch = useDispatch();
return (
<div className={`version`}>
<svg
className={`app`}
onClick={() => {
// triggerInstallApp();
dispatch(openDialog());
}}
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
strokeWidth="2"
stroke="currentColor"
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path
d="M9 3h-4a2 2 0 0 0 -2 2v4a2 2 0 0 0 2 2h4a2 2 0 0 0 2 -2v-4a2 2 0 0 0 -2 -2z"
strokeWidth="0"
fill="currentColor"
/>
<path
d="M9 13h-4a2 2 0 0 0 -2 2v4a2 2 0 0 0 2 2h4a2 2 0 0 0 2 -2v-4a2 2 0 0 0 -2 -2z"
strokeWidth="0"
fill="currentColor"
/>
<path
d="M19 13h-4a2 2 0 0 0 -2 2v4a2 2 0 0 0 2 2h4a2 2 0 0 0 2 -2v-4a2 2 0 0 0 -2 -2z"
strokeWidth="0"
fill="currentColor"
/>
<path
d="M17 3a1 1 0 0 1 .993 .883l.007 .117v2h2a1 1 0 0 1 .117 1.993l-.117 .007h-2v2a1 1 0 0 1 -1.993 .117l-.007 -.117v-2h-2a1 1 0 0 1 -.117 -1.993l.117 -.007h2v-2a1 1 0 0 1 1 -1z"
strokeWidth="0"
fill="currentColor"
/>
</svg>
chatnio v{version}
</div>
);
}
export default ChatFooter;

View File

@ -1,61 +1,37 @@
import { useEffect, useRef, useState } from "react";
import React, { useEffect } from "react";
import { Message } from "@/conversation/types.ts";
import { useSelector } from "react-redux";
import { selectCurrent, selectMessages } from "@/store/chat.ts";
import { Button } from "@/components/ui/button.tsx";
import { ChevronDown } from "lucide-react";
import MessageSegment from "@/components/Message.tsx";
import { connectionEvent } from "@/events/connection.ts";
import { chatEvent } from "@/events/chat.ts";
function ChatInterface() {
const ref = useRef(null);
const [scroll, setScroll] = useState(false);
type ChatInterfaceProps = {
setTarget: (target: HTMLDivElement | null) => void;
};
function ChatInterface({ setTarget }: ChatInterfaceProps) {
const ref = React.useRef(null);
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();
chatEvent.emit();
},
[messages],
);
useEffect(() => {
if (!ref.current) return;
const el = ref.current as HTMLDivElement;
el.addEventListener("scroll", listenScrolling);
setTarget(ref.current);
}, [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}

View File

@ -0,0 +1,84 @@
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { useSelector } from "react-redux";
import { isSubscribedSelector } from "@/store/subscription.ts";
import { Button } from "@/components/ui/button.tsx";
import {
BookMarked,
ChevronRight,
FolderKanban,
Newspaper,
Users2,
} from "lucide-react";
import router from "@/router.tsx";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog.tsx";
function ChatSpace() {
const [open, setOpen] = useState(false);
const { t } = useTranslation();
const subscription = useSelector(isSubscribedSelector);
return (
<div className={`chat-product`}>
<Button variant={`outline`} onClick={() => setOpen(true)}>
<Users2 className={`h-4 w-4 mr-1.5`} />
{t("contact.title")}
<ChevronRight className={`h-4 w-4 ml-2`} />
</Button>
{subscription && (
<Button variant={`outline`} onClick={() => router.navigate("/article")}>
<Newspaper className={`h-4 w-4 mr-1.5`} />
{t("article.title")}
<ChevronRight className={`h-4 w-4 ml-2`} />
</Button>
)}
<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>
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>{t("contact.title")}</DialogTitle>
<DialogDescription asChild>
<div className={`grid pt-4`}>
<Button
className={`mx-auto`}
variant={`outline`}
onClick={() =>
window.open("https://docs.chatnio.net", "_blank")
}
>
<BookMarked className={`h-4 w-4 mr-1.5`} />
{t("docs.title")}
</Button>
<a
href={
"http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=1oKfIbNVXmMNMVzW1NiFSTKDcT1qIEq5&authKey=uslxslIBZtLImf4BSxjDqfx4hiJA52YV7PFM38W%2BOArr%2BhE0jwVdQCRYs0%2FXKX7W&noverify=0&group_code=565902327"
}
target={"_blank"}
className={`inline-flex mx-auto mt-1 mb-2`}
>
<img
src={`/source/qq.jpg`}
className={`contact-image`}
alt={`QQ`}
/>
</a>
</div>
</DialogDescription>
</DialogHeader>
</DialogContent>
</Dialog>
</div>
);
}
export default ChatSpace;

View File

@ -1,118 +1,26 @@
import { useTranslation } from "react-i18next";
import React, { useEffect, useRef, useState } from "react";
import { useEffect, useRef, useState } from "react";
import FileProvider from "@/components/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 { selectMessages, selectModel, selectWeb } from "@/store/chat.ts";
import { manager } from "@/conversation/manager.ts";
import { formatMessage } from "@/utils/processor.ts";
import ChatInterface from "@/components/home/ChatInterface.tsx";
import { Button } from "@/components/ui/button.tsx";
import router from "@/router.tsx";
import {
BookMarked,
ChevronRight,
FolderKanban,
Globe,
Newspaper,
Users2,
} from "lucide-react";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip.tsx";
import { Toggle } from "@/components/ui/toggle.tsx";
import { Input } from "@/components/ui/input.tsx";
import EditorProvider from "@/components/EditorProvider.tsx";
import ModelSelector from "./ModelSelector.tsx";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog.tsx";
import { version } from "@/conf.ts";
import ModelFinder from "./ModelFinder.tsx";
import { clearHistoryState, getQueryParam } from "@/utils/path.ts";
import { forgetMemory, popMemory, setMemory } from "@/utils/memory.ts";
import { forgetMemory, popMemory } from "@/utils/memory.ts";
import { useToast } from "@/components/ui/use-toast.ts";
import { ToastAction } from "@/components/ui/toast.tsx";
import {
alignSelector,
contextSelector,
openDialog,
} from "@/store/settings.ts";
import { isSubscribedSelector } from "@/store/subscription.ts";
import { alignSelector, contextSelector } from "@/store/settings.ts";
import { FileArray } from "@/conversation/file.ts";
function ChatSpace() {
const [open, setOpen] = useState(false);
const { t } = useTranslation();
const subscription = useSelector(isSubscribedSelector);
return (
<div className={`chat-product`}>
<Button variant={`outline`} onClick={() => setOpen(true)}>
<Users2 className={`h-4 w-4 mr-1.5`} />
{t("contact.title")}
<ChevronRight className={`h-4 w-4 ml-2`} />
</Button>
{subscription && (
<Button variant={`outline`} onClick={() => router.navigate("/article")}>
<Newspaper className={`h-4 w-4 mr-1.5`} />
{t("article.title")}
<ChevronRight className={`h-4 w-4 ml-2`} />
</Button>
)}
<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>
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>{t("contact.title")}</DialogTitle>
<DialogDescription asChild>
<div className={`grid pt-4`}>
<Button
className={`mx-auto`}
variant={`outline`}
onClick={() =>
window.open("https://docs.chatnio.net", "_blank")
}
>
<BookMarked className={`h-4 w-4 mr-1.5`} />
{t("docs.title")}
</Button>
<a
href={
"http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=1oKfIbNVXmMNMVzW1NiFSTKDcT1qIEq5&authKey=uslxslIBZtLImf4BSxjDqfx4hiJA52YV7PFM38W%2BOArr%2BhE0jwVdQCRYs0%2FXKX7W&noverify=0&group_code=565902327"
}
target={"_blank"}
className={`inline-flex mx-auto mt-1 mb-2`}
>
<img
src={`/source/qq.jpg`}
className={`contact-image`}
alt={`QQ`}
/>
</a>
</div>
</DialogDescription>
</DialogHeader>
</DialogContent>
</Dialog>
</div>
);
}
import WebToggle from "@/components/home/components/WebToggle.tsx";
import ChatSpace from "@/components/home/ChatSpace.tsx";
import ChatFooter from "@/components/home/ChatFooter.tsx";
import SendButton from "@/components/home/components/SendButton.tsx";
import ChatInput from "@/components/home/components/ChatInput.tsx";
import ScrollAction from "@/components/home/components/ScrollAction.tsx";
function ChatWrapper() {
const { t } = useTranslation();
@ -129,6 +37,8 @@ function ChatWrapper() {
const context = useSelector(contextSelector);
const align = useSelector(alignSelector);
const [instance, setInstance] = useState<HTMLElement | null>(null);
manager.setDispatch(dispatch);
function clearFile() {
@ -204,119 +114,36 @@ function ChatWrapper() {
return (
<div className={`chat-container`}>
<div className={`chat-wrapper`}>
{messages.length > 0 ? <ChatInterface /> : <ChatSpace />}
{messages.length > 0 ? (
<ChatInterface setTarget={setInstance} />
) : (
<ChatSpace />
)}
<ScrollAction target={instance} />
<div className={`chat-input`}>
<div className={`input-wrapper`}>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Toggle
aria-label={t("chat.web-aria")}
defaultPressed={false}
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>
<WebToggle />
<div className={`chat-box`}>
<FileProvider
value={files}
onChange={setFiles}
id={`file`}
className={`file`}
/>
<Input
id={`input`}
className={`input-box ${align && "align"}`}
<FileProvider value={files} onChange={setFiles} />
<ChatInput
className={align ? "align" : ""}
ref={target}
value={input}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setInput(e.target.value);
setMemory("history", e.target.value);
}}
placeholder={t("chat.placeholder")}
onKeyDown={async (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Enter") await handleSend(auth, model, web);
}}
onValueChange={setInput}
onEnterPressed={async () => 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>
<SendButton onClick={() => handleSend(auth, model, web)} />
</div>
<div className={`input-options`}>
<ModelSelector side={`bottom`} />
</div>
<div className={`version`}>
<svg
className={`app`}
onClick={() => {
// triggerInstallApp();
dispatch(openDialog());
}}
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
strokeWidth="2"
stroke="currentColor"
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path
d="M9 3h-4a2 2 0 0 0 -2 2v4a2 2 0 0 0 2 2h4a2 2 0 0 0 2 -2v-4a2 2 0 0 0 -2 -2z"
strokeWidth="0"
fill="currentColor"
/>
<path
d="M9 13h-4a2 2 0 0 0 -2 2v4a2 2 0 0 0 2 2h4a2 2 0 0 0 2 -2v-4a2 2 0 0 0 -2 -2z"
strokeWidth="0"
fill="currentColor"
/>
<path
d="M19 13h-4a2 2 0 0 0 -2 2v4a2 2 0 0 0 2 2h4a2 2 0 0 0 2 -2v-4a2 2 0 0 0 -2 -2z"
strokeWidth="0"
fill="currentColor"
/>
<path
d="M17 3a1 1 0 0 1 .993 .883l.007 .117v2h2a1 1 0 0 1 .117 1.993l-.117 .007h-2v2a1 1 0 0 1 -1.993 .117l-.007 -.117v-2h-2a1 1 0 0 1 -.117 -1.993l.117 -.007h2v-2a1 1 0 0 1 1 -1z"
strokeWidth="0"
fill="currentColor"
/>
</svg>
chatnio v{version}
<ModelFinder side={`bottom`} />
</div>
<ChatFooter />
</div>
</div>
</div>

View File

@ -19,7 +19,7 @@ type ModelSelectorProps = {
side?: "left" | "right" | "top" | "bottom";
};
function ModelSelector(props: ModelSelectorProps) {
function ModelFinder(props: ModelSelectorProps) {
const { t } = useTranslation();
const dispatch = useDispatch();
const { toast } = useToast();
@ -63,7 +63,7 @@ function ModelSelector(props: ModelSelectorProps) {
<SelectGroup
current={list.find((item) => item.name === model) as SelectItemProps}
list={list}
maxElements={6}
maxElements={5}
side={props.side}
classNameMobile={`model-select-group`}
onChange={(value: string) => {
@ -87,4 +87,4 @@ function ModelSelector(props: ModelSelectorProps) {
);
}
export default ModelSelector;
export default ModelFinder;

View File

@ -0,0 +1,41 @@
import { Input } from "@/components/ui/input.tsx";
import React from "react";
import { setMemory } from "@/utils/memory.ts";
import { useTranslation } from "react-i18next";
type ChatInputProps = {
className?: string;
ref?: React.RefObject<HTMLInputElement>;
value: string;
onValueChange: (value: string) => void;
onEnterPressed: () => void;
};
function ChatInput({
className,
ref,
value,
onValueChange,
onEnterPressed,
}: ChatInputProps) {
const { t } = useTranslation();
return (
<Input
id={`input`}
className={`input-box ${className || ""}`}
ref={ref}
value={value}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
onValueChange(e.target.value);
setMemory("history", e.target.value);
}}
placeholder={t("chat.placeholder")}
onKeyDown={async (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Enter") onEnterPressed();
}}
/>
);
}
export default ChatInput;

View File

@ -0,0 +1,45 @@
import { Button } from "@/components/ui/button.tsx";
import { ChevronDown } from "lucide-react";
import { useEffect, useState } from "react";
import { chatEvent } from "@/events/chat.ts";
type ScrollActionProps = {
target: HTMLElement | null;
};
function ScrollAction({ target }: ScrollActionProps) {
const [enabled, setEnabled] = useState(false);
useEffect(() => {
if (!target) return;
target.addEventListener("scroll", listenScrollingAction);
}, [target]);
function listenScrollingAction() {
if (!target) return;
const offset = target.scrollHeight - target.scrollTop - target.clientHeight;
setEnabled(offset > 100);
}
chatEvent.addEventListener(listenScrollingAction);
return (
<div className={`scroll-action ${enabled ? "active" : ""}`}>
<Button
variant={`outline`}
size={`icon`}
onClick={() => {
if (!target) return;
target.scrollTo({
top: target.scrollHeight,
behavior: "smooth",
});
}}
>
<ChevronDown className={`h-4 w-4`} />
</Button>
</div>
);
}
export default ScrollAction;

View File

@ -0,0 +1,28 @@
import { Button } from "@/components/ui/button.tsx";
type SendButtonProps = {
onClick: () => any;
};
function SendButton({ onClick }: SendButtonProps) {
return (
<Button
size={`icon`}
variant="outline"
className={`send-button`}
onClick={onClick}
>
<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>
);
}
export default SendButton;

View File

@ -0,0 +1,41 @@
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip.tsx";
import { Toggle } from "@/components/ui/toggle.tsx";
import { selectWeb, setWeb } from "@/store/chat.ts";
import { Globe } from "lucide-react";
import { useDispatch, useSelector } from "react-redux";
import { useTranslation } from "react-i18next";
function WebToggle() {
const { t } = useTranslation();
const dispatch = useDispatch();
const web = useSelector(selectWeb);
return (
<>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Toggle
aria-label={t("chat.web-aria")}
defaultPressed={false}
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>
</>
);
}
export default WebToggle;

View File

@ -31,7 +31,7 @@ export const supportModels: Model[] = [
// spark desk
{ id: "spark-desk-v3", name: "讯飞星火 V3", free: true, auth: true },
{ id: "spark-desk-v2", name: "讯飞星火 V2", free: true, auth: true },
{ id: "spark-desk-v2", name: "讯飞星火 V1.5", free: true, auth: true },
{ id: "spark-desk-v1.5", name: "讯飞星火 V1.5", free: true, auth: true },
// dashscope models
{ id: "qwen-plus-net", name: "通义千问 Plus X", free: false, auth: true },

5
app/src/events/chat.ts Normal file
View File

@ -0,0 +1,5 @@
import { EventCommitter } from "@/events/struct.ts";
export const chatEvent = new EventCommitter<void>({
name: "chat",
});

View File

@ -13,7 +13,7 @@ import {
CardTitle,
} from "@/components/ui/card.tsx";
import { useState } from "react";
import ModelSelector from "@/components/home/ModelSelector.tsx";
import ModelFinder from "@/components/home/ModelFinder.tsx";
import { Toggle } from "@/components/ui/toggle.tsx";
import { selectModel, selectWeb, setWeb } from "@/store/chat.ts";
import { Label } from "@/components/ui/label.tsx";
@ -175,7 +175,7 @@ function ArticleContent() {
value={title}
onChange={(e) => setTitle(e.target.value)}
/>
<ModelSelector side={`bottom`} />
<ModelFinder side={`bottom`} />
<Button
variant={`default`}
className={`mt-5 w-full mx-auto`}

View File

@ -11,7 +11,7 @@ import { manager } from "@/conversation/generation.ts";
import { useToast } from "@/components/ui/use-toast.ts";
import { handleGenerationData } from "@/utils/processor.ts";
import { selectModel } from "@/store/chat.ts";
import ModelSelector from "@/components/home/ModelSelector.tsx";
import ModelFinder from "@/components/home/ModelFinder.tsx";
type WrapperProps = {
onSend?: (value: string, model: string) => boolean;
@ -147,7 +147,7 @@ function Wrapper({ onSend }: WrapperProps) {
</Button>
</div>
<div className={`model-box`}>
<ModelSelector side={`bottom`} />
<ModelFinder side={`bottom`} />
</div>
</div>
);

View File

@ -27,7 +27,7 @@ const chatSlice = createSlice({
history: [],
messages: [],
model: GetModel(getMemory("model")),
web: false,
web: getMemory("web") === "true",
current: -1,
} as initialStateType,
reducers: {
@ -57,6 +57,7 @@ const chatSlice = createSlice({
state.model = action.payload as string;
},
setWeb: (state, action) => {
setMemory("web", action.payload ? "true" : "false");
state.web = action.payload as boolean;
},
setCurrent: (state, action) => {