mirror of
https://github.com/coaidev/coai.git
synced 2025-05-22 14:30:14 +09:00
update scroll action and fix file style in mobile
This commit is contained in:
parent
76235ca796
commit
5a20c5282e
@ -22,6 +22,7 @@
|
|||||||
.file-wrapper {
|
.file-wrapper {
|
||||||
margin-top: 24px !important;
|
margin-top: 24px !important;
|
||||||
padding: 8px 0;
|
padding: 8px 0;
|
||||||
|
max-width: calc(90vw - 3rem);
|
||||||
}
|
}
|
||||||
|
|
||||||
.drop-window {
|
.drop-window {
|
||||||
@ -103,3 +104,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.file-name {
|
||||||
|
word-wrap: anywhere;
|
||||||
|
word-break: break-all;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
@ -13,6 +13,7 @@
|
|||||||
border: 1px solid hsl(var(--border-hover));
|
border: 1px solid hsl(var(--border-hover));
|
||||||
background: hsl(var(--background-container)) !important;
|
background: hsl(var(--background-container)) !important;
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
|
white-space: pre-wrap;
|
||||||
|
|
||||||
svg {
|
svg {
|
||||||
width: 16px;
|
width: 16px;
|
||||||
|
@ -14,10 +14,11 @@
|
|||||||
.scroll-action {
|
.scroll-action {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 12;
|
z-index: 12;
|
||||||
bottom: 112px;
|
|
||||||
right: 36px;
|
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
|
right: 36px;
|
||||||
|
bottom: 12rem;
|
||||||
transition: .25s;
|
transition: .25s;
|
||||||
|
pointer-events: none;
|
||||||
|
|
||||||
button {
|
button {
|
||||||
border-color: rgba(0,0,0,0) !important;
|
border-color: rgba(0,0,0,0) !important;
|
||||||
@ -25,6 +26,11 @@
|
|||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
|
pointer-events: all;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 668px) {
|
||||||
|
bottom: 8.5rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -318,6 +318,7 @@
|
|||||||
padding: 6px 24px;
|
padding: 6px 24px;
|
||||||
|
|
||||||
.input-wrapper {
|
.input-wrapper {
|
||||||
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
@ -19,19 +19,11 @@ import { Button } from "./ui/button.tsx";
|
|||||||
type RichEditorProps = {
|
type RichEditorProps = {
|
||||||
value: string;
|
value: string;
|
||||||
onChange: (value: string) => void;
|
onChange: (value: string) => void;
|
||||||
className?: string;
|
|
||||||
id?: string;
|
|
||||||
placeholder?: string;
|
|
||||||
maxLength?: number;
|
maxLength?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
function RichEditor({
|
function RichEditor({ value, onChange, maxLength }: RichEditorProps) {
|
||||||
value,
|
const { t } = useTranslation();
|
||||||
onChange,
|
|
||||||
id,
|
|
||||||
placeholder,
|
|
||||||
maxLength,
|
|
||||||
}: RichEditorProps) {
|
|
||||||
const input = useRef(null);
|
const input = useRef(null);
|
||||||
const [openPreview, setOpenPreview] = useState(!mobile);
|
const [openPreview, setOpenPreview] = useState(!mobile);
|
||||||
const [openInput, setOpenInput] = useState(true);
|
const [openInput, setOpenInput] = useState(true);
|
||||||
@ -117,10 +109,10 @@ function RichEditor({
|
|||||||
>
|
>
|
||||||
{openInput && (
|
{openInput && (
|
||||||
<Textarea
|
<Textarea
|
||||||
placeholder={placeholder}
|
placeholder={t("chat.placeholder")}
|
||||||
value={value}
|
value={value}
|
||||||
className={`editor-input`}
|
className={`editor-input`}
|
||||||
id={id}
|
id={`editor`}
|
||||||
maxLength={maxLength}
|
maxLength={maxLength}
|
||||||
onChange={(e) => onChange(e.target.value)}
|
onChange={(e) => onChange(e.target.value)}
|
||||||
ref={input}
|
ref={input}
|
||||||
@ -142,7 +134,7 @@ function EditorProvider(props: RichEditorProps) {
|
|||||||
<>
|
<>
|
||||||
<Dialog>
|
<Dialog>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<div className={`editor-action active ${props.className}`}>
|
<div className={`editor-action active editor`}>
|
||||||
<Maximize className={`h-3.5 w-3.5`} />
|
<Maximize className={`h-3.5 w-3.5`} />
|
||||||
</div>
|
</div>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
|
@ -31,14 +31,11 @@ const MaxFileSize = 1024 * 1024 * 25; // 25MB File Size Limit
|
|||||||
const MaxPromptSize = 5000; // 5000 Prompt Size Limit (to avoid token overflow)
|
const MaxPromptSize = 5000; // 5000 Prompt Size Limit (to avoid token overflow)
|
||||||
|
|
||||||
type FileProviderProps = {
|
type FileProviderProps = {
|
||||||
id: string;
|
|
||||||
className?: string;
|
|
||||||
|
|
||||||
value: FileArray;
|
value: FileArray;
|
||||||
onChange?: (value: FileArray) => void;
|
onChange?: (value: FileArray) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
function FileProvider({ id, className, value, onChange }: FileProviderProps) {
|
function FileProvider({ value, onChange }: FileProviderProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const model = useSelector(selectModel);
|
const model = useSelector(selectModel);
|
||||||
@ -82,7 +79,7 @@ function FileProvider({ id, className, value, onChange }: FileProviderProps) {
|
|||||||
<AlertTitle>{t("file.type")}</AlertTitle>
|
<AlertTitle>{t("file.type")}</AlertTitle>
|
||||||
</Alert>
|
</Alert>
|
||||||
<FileList value={value} removeFile={removeFile} />
|
<FileList value={value} removeFile={removeFile} />
|
||||||
<FileInput id={id} className={className} addFile={addFile} />
|
<FileInput id={"file"} className={"file"} addFile={addFile} />
|
||||||
</div>
|
</div>
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
@ -111,7 +108,7 @@ function FileList({ value, removeFile }: FileListProps) {
|
|||||||
<div className={`file-list`}>
|
<div className={`file-list`}>
|
||||||
{value.length > 3 && full && (
|
{value.length > 3 && full && (
|
||||||
<div className={`file-item`}>
|
<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`}>
|
<div className={`file-name mr-1`}>
|
||||||
{t("file.number", { number: value.length })}
|
{t("file.number", { number: value.length })}
|
||||||
</div>
|
</div>
|
||||||
@ -129,7 +126,7 @@ function FileList({ value, removeFile }: FileListProps) {
|
|||||||
{value.length <= 3 || full ? (
|
{value.length <= 3 || full ? (
|
||||||
value.map((file, index) => (
|
value.map((file, index) => (
|
||||||
<div className={`file-item`} key={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={`file-name mr-1`}>{file.name}</div>
|
||||||
<div className={`grow`} />
|
<div className={`grow`} />
|
||||||
<div className={`file-size mr-2`}>
|
<div className={`file-size mr-2`}>
|
||||||
@ -147,7 +144,7 @@ function FileList({ value, removeFile }: FileListProps) {
|
|||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<div className={`file-item`}>
|
<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`}>
|
<div className={`file-name mr-1`}>
|
||||||
{t("file.zipper", {
|
{t("file.zipper", {
|
||||||
filename: file.name,
|
filename: file.name,
|
||||||
|
@ -27,6 +27,7 @@ function ReloadPrompt() {
|
|||||||
|
|
||||||
const before = getMemory("version");
|
const before = getMemory("version");
|
||||||
if (before.length > 0 && before !== version) {
|
if (before.length > 0 && before !== version) {
|
||||||
|
setMemory("version", version);
|
||||||
toast({
|
toast({
|
||||||
title: t("service.update-success"),
|
title: t("service.update-success"),
|
||||||
description: t("service.update-success-prompt"),
|
description: t("service.update-success-prompt"),
|
||||||
|
53
app/src/components/home/ChatFooter.tsx
Normal file
53
app/src/components/home/ChatFooter.tsx
Normal 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;
|
@ -1,61 +1,37 @@
|
|||||||
import { useEffect, useRef, useState } from "react";
|
import React, { useEffect } from "react";
|
||||||
import { Message } from "@/conversation/types.ts";
|
import { Message } from "@/conversation/types.ts";
|
||||||
import { useSelector } from "react-redux";
|
import { useSelector } from "react-redux";
|
||||||
import { selectCurrent, selectMessages } from "@/store/chat.ts";
|
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 MessageSegment from "@/components/Message.tsx";
|
||||||
import { connectionEvent } from "@/events/connection.ts";
|
import { connectionEvent } from "@/events/connection.ts";
|
||||||
|
import { chatEvent } from "@/events/chat.ts";
|
||||||
|
|
||||||
function ChatInterface() {
|
type ChatInterfaceProps = {
|
||||||
const ref = useRef(null);
|
setTarget: (target: HTMLDivElement | null) => void;
|
||||||
const [scroll, setScroll] = useState(false);
|
};
|
||||||
|
|
||||||
|
function ChatInterface({ setTarget }: ChatInterfaceProps) {
|
||||||
|
const ref = React.useRef(null);
|
||||||
const messages: Message[] = useSelector(selectMessages);
|
const messages: Message[] = useSelector(selectMessages);
|
||||||
const current: number = useSelector(selectCurrent);
|
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(
|
useEffect(
|
||||||
function () {
|
function () {
|
||||||
if (!ref.current) return;
|
if (!ref.current) return;
|
||||||
const el = ref.current as HTMLDivElement;
|
const el = ref.current as HTMLDivElement;
|
||||||
el.scrollTop = el.scrollHeight;
|
el.scrollTop = el.scrollHeight;
|
||||||
listenScrolling();
|
chatEvent.emit();
|
||||||
},
|
},
|
||||||
[messages],
|
[messages],
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!ref.current) return;
|
setTarget(ref.current);
|
||||||
const el = ref.current as HTMLDivElement;
|
|
||||||
el.addEventListener("scroll", listenScrolling);
|
|
||||||
}, [ref]);
|
}, [ref]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className={`chat-content`} ref={ref}>
|
<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) => (
|
{messages.map((message, i) => (
|
||||||
<MessageSegment
|
<MessageSegment
|
||||||
message={message}
|
message={message}
|
||||||
|
84
app/src/components/home/ChatSpace.tsx
Normal file
84
app/src/components/home/ChatSpace.tsx
Normal 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;
|
@ -1,118 +1,26 @@
|
|||||||
import { useTranslation } from "react-i18next";
|
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 FileProvider from "@/components/FileProvider.tsx";
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
import { selectAuthenticated, selectInit } from "@/store/auth.ts";
|
import { selectAuthenticated, selectInit } from "@/store/auth.ts";
|
||||||
import {
|
import { selectMessages, selectModel, selectWeb } from "@/store/chat.ts";
|
||||||
selectMessages,
|
|
||||||
selectModel,
|
|
||||||
selectWeb,
|
|
||||||
setWeb,
|
|
||||||
} from "@/store/chat.ts";
|
|
||||||
import { manager } from "@/conversation/manager.ts";
|
import { manager } from "@/conversation/manager.ts";
|
||||||
import { formatMessage } from "@/utils/processor.ts";
|
import { formatMessage } from "@/utils/processor.ts";
|
||||||
import ChatInterface from "@/components/home/ChatInterface.tsx";
|
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 EditorProvider from "@/components/EditorProvider.tsx";
|
||||||
import ModelSelector from "./ModelSelector.tsx";
|
import ModelFinder from "./ModelFinder.tsx";
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from "@/components/ui/dialog.tsx";
|
|
||||||
import { version } from "@/conf.ts";
|
|
||||||
import { clearHistoryState, getQueryParam } from "@/utils/path.ts";
|
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 { useToast } from "@/components/ui/use-toast.ts";
|
||||||
import { ToastAction } from "@/components/ui/toast.tsx";
|
import { ToastAction } from "@/components/ui/toast.tsx";
|
||||||
import {
|
import { alignSelector, contextSelector } from "@/store/settings.ts";
|
||||||
alignSelector,
|
|
||||||
contextSelector,
|
|
||||||
openDialog,
|
|
||||||
} from "@/store/settings.ts";
|
|
||||||
import { isSubscribedSelector } from "@/store/subscription.ts";
|
|
||||||
import { FileArray } from "@/conversation/file.ts";
|
import { FileArray } from "@/conversation/file.ts";
|
||||||
|
import WebToggle from "@/components/home/components/WebToggle.tsx";
|
||||||
function ChatSpace() {
|
import ChatSpace from "@/components/home/ChatSpace.tsx";
|
||||||
const [open, setOpen] = useState(false);
|
import ChatFooter from "@/components/home/ChatFooter.tsx";
|
||||||
const { t } = useTranslation();
|
import SendButton from "@/components/home/components/SendButton.tsx";
|
||||||
const subscription = useSelector(isSubscribedSelector);
|
import ChatInput from "@/components/home/components/ChatInput.tsx";
|
||||||
return (
|
import ScrollAction from "@/components/home/components/ScrollAction.tsx";
|
||||||
<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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ChatWrapper() {
|
function ChatWrapper() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@ -129,6 +37,8 @@ function ChatWrapper() {
|
|||||||
const context = useSelector(contextSelector);
|
const context = useSelector(contextSelector);
|
||||||
const align = useSelector(alignSelector);
|
const align = useSelector(alignSelector);
|
||||||
|
|
||||||
|
const [instance, setInstance] = useState<HTMLElement | null>(null);
|
||||||
|
|
||||||
manager.setDispatch(dispatch);
|
manager.setDispatch(dispatch);
|
||||||
|
|
||||||
function clearFile() {
|
function clearFile() {
|
||||||
@ -204,119 +114,36 @@ function ChatWrapper() {
|
|||||||
return (
|
return (
|
||||||
<div className={`chat-container`}>
|
<div className={`chat-container`}>
|
||||||
<div className={`chat-wrapper`}>
|
<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={`chat-input`}>
|
||||||
<div className={`input-wrapper`}>
|
<div className={`input-wrapper`}>
|
||||||
<TooltipProvider>
|
<WebToggle />
|
||||||
<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>
|
|
||||||
<div className={`chat-box`}>
|
<div className={`chat-box`}>
|
||||||
<FileProvider
|
<FileProvider value={files} onChange={setFiles} />
|
||||||
value={files}
|
<ChatInput
|
||||||
onChange={setFiles}
|
className={align ? "align" : ""}
|
||||||
id={`file`}
|
|
||||||
className={`file`}
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
id={`input`}
|
|
||||||
className={`input-box ${align && "align"}`}
|
|
||||||
ref={target}
|
ref={target}
|
||||||
value={input}
|
value={input}
|
||||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
onValueChange={setInput}
|
||||||
setInput(e.target.value);
|
onEnterPressed={async () => await handleSend(auth, model, web)}
|
||||||
setMemory("history", e.target.value);
|
|
||||||
}}
|
|
||||||
placeholder={t("chat.placeholder")}
|
|
||||||
onKeyDown={async (e: React.KeyboardEvent<HTMLInputElement>) => {
|
|
||||||
if (e.key === "Enter") await handleSend(auth, model, web);
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
<EditorProvider
|
<EditorProvider
|
||||||
value={input}
|
value={input}
|
||||||
onChange={setInput}
|
onChange={setInput}
|
||||||
className={`editor`}
|
|
||||||
id={`editor`}
|
|
||||||
placeholder={t("chat.placeholder")}
|
|
||||||
maxLength={8000}
|
maxLength={8000}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<SendButton onClick={() => handleSend(auth, model, web)} />
|
||||||
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>
|
||||||
<div className={`input-options`}>
|
<div className={`input-options`}>
|
||||||
<ModelSelector side={`bottom`} />
|
<ModelFinder 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}
|
|
||||||
</div>
|
</div>
|
||||||
|
<ChatFooter />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -19,7 +19,7 @@ type ModelSelectorProps = {
|
|||||||
side?: "left" | "right" | "top" | "bottom";
|
side?: "left" | "right" | "top" | "bottom";
|
||||||
};
|
};
|
||||||
|
|
||||||
function ModelSelector(props: ModelSelectorProps) {
|
function ModelFinder(props: ModelSelectorProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
@ -63,7 +63,7 @@ function ModelSelector(props: ModelSelectorProps) {
|
|||||||
<SelectGroup
|
<SelectGroup
|
||||||
current={list.find((item) => item.name === model) as SelectItemProps}
|
current={list.find((item) => item.name === model) as SelectItemProps}
|
||||||
list={list}
|
list={list}
|
||||||
maxElements={6}
|
maxElements={5}
|
||||||
side={props.side}
|
side={props.side}
|
||||||
classNameMobile={`model-select-group`}
|
classNameMobile={`model-select-group`}
|
||||||
onChange={(value: string) => {
|
onChange={(value: string) => {
|
||||||
@ -87,4 +87,4 @@ function ModelSelector(props: ModelSelectorProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ModelSelector;
|
export default ModelFinder;
|
41
app/src/components/home/components/ChatInput.tsx
Normal file
41
app/src/components/home/components/ChatInput.tsx
Normal 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;
|
45
app/src/components/home/components/ScrollAction.tsx
Normal file
45
app/src/components/home/components/ScrollAction.tsx
Normal 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;
|
28
app/src/components/home/components/SendButton.tsx
Normal file
28
app/src/components/home/components/SendButton.tsx
Normal 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;
|
41
app/src/components/home/components/WebToggle.tsx
Normal file
41
app/src/components/home/components/WebToggle.tsx
Normal 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;
|
@ -31,7 +31,7 @@ export const supportModels: Model[] = [
|
|||||||
// spark desk
|
// spark desk
|
||||||
{ id: "spark-desk-v3", name: "讯飞星火 V3", free: true, auth: true },
|
{ 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: "讯飞星火 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
|
// dashscope models
|
||||||
{ id: "qwen-plus-net", name: "通义千问 Plus X", free: false, auth: true },
|
{ id: "qwen-plus-net", name: "通义千问 Plus X", free: false, auth: true },
|
||||||
|
5
app/src/events/chat.ts
Normal file
5
app/src/events/chat.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { EventCommitter } from "@/events/struct.ts";
|
||||||
|
|
||||||
|
export const chatEvent = new EventCommitter<void>({
|
||||||
|
name: "chat",
|
||||||
|
});
|
@ -13,7 +13,7 @@ import {
|
|||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@/components/ui/card.tsx";
|
} from "@/components/ui/card.tsx";
|
||||||
import { useState } from "react";
|
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 { Toggle } from "@/components/ui/toggle.tsx";
|
||||||
import { selectModel, selectWeb, setWeb } from "@/store/chat.ts";
|
import { selectModel, selectWeb, setWeb } from "@/store/chat.ts";
|
||||||
import { Label } from "@/components/ui/label.tsx";
|
import { Label } from "@/components/ui/label.tsx";
|
||||||
@ -175,7 +175,7 @@ function ArticleContent() {
|
|||||||
value={title}
|
value={title}
|
||||||
onChange={(e) => setTitle(e.target.value)}
|
onChange={(e) => setTitle(e.target.value)}
|
||||||
/>
|
/>
|
||||||
<ModelSelector side={`bottom`} />
|
<ModelFinder side={`bottom`} />
|
||||||
<Button
|
<Button
|
||||||
variant={`default`}
|
variant={`default`}
|
||||||
className={`mt-5 w-full mx-auto`}
|
className={`mt-5 w-full mx-auto`}
|
||||||
|
@ -11,7 +11,7 @@ import { manager } from "@/conversation/generation.ts";
|
|||||||
import { useToast } from "@/components/ui/use-toast.ts";
|
import { useToast } from "@/components/ui/use-toast.ts";
|
||||||
import { handleGenerationData } from "@/utils/processor.ts";
|
import { handleGenerationData } from "@/utils/processor.ts";
|
||||||
import { selectModel } from "@/store/chat.ts";
|
import { selectModel } from "@/store/chat.ts";
|
||||||
import ModelSelector from "@/components/home/ModelSelector.tsx";
|
import ModelFinder from "@/components/home/ModelFinder.tsx";
|
||||||
|
|
||||||
type WrapperProps = {
|
type WrapperProps = {
|
||||||
onSend?: (value: string, model: string) => boolean;
|
onSend?: (value: string, model: string) => boolean;
|
||||||
@ -147,7 +147,7 @@ function Wrapper({ onSend }: WrapperProps) {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className={`model-box`}>
|
<div className={`model-box`}>
|
||||||
<ModelSelector side={`bottom`} />
|
<ModelFinder side={`bottom`} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -27,7 +27,7 @@ const chatSlice = createSlice({
|
|||||||
history: [],
|
history: [],
|
||||||
messages: [],
|
messages: [],
|
||||||
model: GetModel(getMemory("model")),
|
model: GetModel(getMemory("model")),
|
||||||
web: false,
|
web: getMemory("web") === "true",
|
||||||
current: -1,
|
current: -1,
|
||||||
} as initialStateType,
|
} as initialStateType,
|
||||||
reducers: {
|
reducers: {
|
||||||
@ -57,6 +57,7 @@ const chatSlice = createSlice({
|
|||||||
state.model = action.payload as string;
|
state.model = action.payload as string;
|
||||||
},
|
},
|
||||||
setWeb: (state, action) => {
|
setWeb: (state, action) => {
|
||||||
|
setMemory("web", action.payload ? "true" : "false");
|
||||||
state.web = action.payload as boolean;
|
state.web = action.payload as boolean;
|
||||||
},
|
},
|
||||||
setCurrent: (state, action) => {
|
setCurrent: (state, action) => {
|
||||||
|
Loading…
Reference in New Issue
Block a user