update history and chat storage

This commit is contained in:
Zhang Minghan 2023-09-04 21:21:41 +08:00
parent 36ea2ba04e
commit 4fbc8f24b1
16 changed files with 588 additions and 38 deletions

View File

@ -10,6 +10,7 @@
"preview": "vite preview"
},
"dependencies": {
"@radix-ui/react-alert-dialog": "^1.0.4",
"@radix-ui/react-dropdown-menu": "^2.0.5",
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-slot": "^1.0.2",

63
renio/pnpm-lock.yaml generated
View File

@ -5,6 +5,9 @@ settings:
excludeLinksFromLockfile: false
dependencies:
'@radix-ui/react-alert-dialog':
specifier: ^1.0.4
version: 1.0.4(@types/react-dom@18.2.7)(@types/react@18.2.15)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-dropdown-menu':
specifier: ^2.0.5
version: 2.0.5(@types/react-dom@18.2.7)(@types/react@18.2.15)(react-dom@18.2.0)(react@18.2.0)
@ -482,6 +485,32 @@ packages:
'@babel/runtime': 7.22.11
dev: false
/@radix-ui/react-alert-dialog@1.0.4(@types/react-dom@18.2.7)(@types/react@18.2.15)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-jbfBCRlKYlhbitueOAv7z74PXYeIQmWpKwm3jllsdkw7fGWNkxqP3v0nY9WmOzcPqpQuoorNtvViBgL46n5gVg==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
dependencies:
'@babel/runtime': 7.22.11
'@radix-ui/primitive': 1.0.1
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.15)(react@18.2.0)
'@radix-ui/react-context': 1.0.1(@types/react@18.2.15)(react@18.2.0)
'@radix-ui/react-dialog': 1.0.4(@types/react-dom@18.2.7)(@types/react@18.2.15)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.7)(@types/react@18.2.15)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-slot': 1.0.2(@types/react@18.2.15)(react@18.2.0)
'@types/react': 18.2.15
'@types/react-dom': 18.2.7
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
dev: false
/@radix-ui/react-arrow@1.0.3(@types/react-dom@18.2.7)(@types/react@18.2.15)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-wSP+pHsB/jQRaL6voubsQ/ZlrGBHHrOjmBnr19hxYgtS0WvAFwZhK2WP/YY5yF9uKECCEEDGxuLxq1NBK51wFA==}
peerDependencies:
@ -555,6 +584,40 @@ packages:
react: 18.2.0
dev: false
/@radix-ui/react-dialog@1.0.4(@types/react-dom@18.2.7)(@types/react@18.2.15)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-hJtRy/jPULGQZceSAP2Re6/4NpKo8im6V8P2hUqZsdFiSL8l35kYsw3qbRI6Ay5mQd2+wlLqje770eq+RJ3yZg==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
dependencies:
'@babel/runtime': 7.22.11
'@radix-ui/primitive': 1.0.1
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.15)(react@18.2.0)
'@radix-ui/react-context': 1.0.1(@types/react@18.2.15)(react@18.2.0)
'@radix-ui/react-dismissable-layer': 1.0.4(@types/react-dom@18.2.7)(@types/react@18.2.15)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-focus-guards': 1.0.1(@types/react@18.2.15)(react@18.2.0)
'@radix-ui/react-focus-scope': 1.0.3(@types/react-dom@18.2.7)(@types/react@18.2.15)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-id': 1.0.1(@types/react@18.2.15)(react@18.2.0)
'@radix-ui/react-portal': 1.0.3(@types/react-dom@18.2.7)(@types/react@18.2.15)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-presence': 1.0.1(@types/react-dom@18.2.7)(@types/react@18.2.15)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.7)(@types/react@18.2.15)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-slot': 1.0.2(@types/react@18.2.15)(react@18.2.0)
'@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.15)(react@18.2.0)
'@types/react': 18.2.15
'@types/react-dom': 18.2.7
aria-hidden: 1.2.3
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
react-remove-scroll: 2.5.5(@types/react@18.2.15)(react@18.2.0)
dev: false
/@radix-ui/react-direction@1.0.1(@types/react@18.2.15)(react@18.2.0):
resolution: {integrity: sha512-RXcvnXgyvYvBEOhCBuddKecVkoMiI10Jcm5cTI7abJRAHYfFxeu+FBQs/DvdxSYucxR5mna0dNsL6QFlds5TMA==}
peerDependencies:

29
renio/qodana.yaml Normal file
View File

@ -0,0 +1,29 @@
#-------------------------------------------------------------------------------#
# Qodana analysis is configured by qodana.yaml file #
# https://www.jetbrains.com/help/qodana/qodana-yaml.html #
#-------------------------------------------------------------------------------#
version: "1.0"
#Specify inspection profile for code analysis
profile:
name: qodana.starter
#Enable inspections
#include:
# - name: <SomeEnabledInspectionId>
#Disable inspections
#exclude:
# - name: <SomeDisabledInspectionId>
# paths:
# - <path/where/not/run/inspection>
#Execute shell command before Qodana execution (Applied in CI/CD pipeline)
#bootstrap: sh ./prepare-qodana.sh
#Install IDE plugins before Qodana execution (Applied in CI/CD pipeline)
#plugins:
# - id: <plugin.id> #(plugin id can be found at https://plugins.jetbrains.com)
#Specify Qodana linter for analysis (Applied in CI/CD pipeline)
linter: jetbrains/qodana-js:latest

View File

@ -35,9 +35,10 @@ function Settings() {
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align={`end`}>
<DropdownMenuLabel>My Account</DropdownMenuLabel>
<DropdownMenuLabel className={`username`}>{ username }</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem>Quota</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem asChild>
<Button size={`sm`} className={`action-button`} onClick={() => dispatch(logout())}>
Logout

View File

@ -9,10 +9,10 @@
--foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--card-foreground: 240 10% 3.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
--popover-foreground: 240 10% 3.9%;
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
@ -20,8 +20,8 @@
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
--muted: 240 4.8% 95.9%;
--muted-foreground: 240 3.8% 46.1%;
--accent: 240 4.8% 95.9%;
--accent-secondary: 240 5.9% 10%;
@ -46,11 +46,11 @@
--background-container: 0 0% 7.8%;
--foreground: 210 40% 98%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
--card: 240 10% 3.9%;
--card-foreground: 0 0% 98%;
--popover: 222.2 84% 4.9%;
--popover-foreground: 210 40% 98%;
--popover: 240 10% 3.9%;
--popover-foreground: 0 0% 98%;
--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 11.2%;
@ -58,8 +58,8 @@
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--muted: 240 3.7% 15.9%;
--muted-foreground: 240 5% 64.9%;
--accent: 240 3.7% 15.9%;
--accent-secondary: 240 3.7% 15.9%;
@ -74,6 +74,9 @@
--text: 0 0% 100%;
--text-secondary: 0 0% 80%;
--conversation-card: rgba(152,153,165,.05);
--conversation-card-hover: rgba(152,153,165,.2);
--conversation-card-active: rgba(152,153,165,.3);
--background-sidebar: rgba(0, 0, 0, 0.25);
}
}

View File

@ -25,6 +25,128 @@
border-right: 1px solid hsl(var(--border));
}
.sidebar-content {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
padding: 4px;
}
&.open .conversation-list {
opacity: 1;
}
.conversation-list {
position: relative;
display: flex;
flex-direction: column;
gap: 6px;
opacity: 0;
width: 100%;
height: 100%;
padding: 6px 0;
overflow-x: hidden;
overflow-y: auto;
touch-action: pan-y;
user-select: none;
scrollbar-width: thin;
transition: 0.2s ease-in-out;
.conversation {
display: flex;
flex-direction: row;
vertical-align: center;
align-items: center;
width: calc(100% - 12px);
height: max-content;
cursor: pointer;
margin: 0 6px;
padding: 10px 12px;
border-radius: var(--radius);
transition: 0.2s ease-in-out;
background: var(--conversation-card);
.delete {
color: hsl(var(--text-secondary));
display: none;
transition: 0.2s;
&:hover {
color: hsl(var(--text));
}
}
&:hover {
background: var(--conversation-card-hover);
.id {
display: none;
}
.delete {
display: block;
}
}
&.active {
background: var(--conversation-card-active);
}
}
svg {
flex-shrink: 0;
}
.title {
flex-grow: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-size: 16px;
user-select: none;
margin: 0 4px;
color: hsl(var(--text));
}
.id {
flex-shrink: 0;
color: hsl(var(--text-secondary));
font-size: 14px;
user-select: none;
&:before {
content: "#";
font-size: 12px;
margin-right: 1px;
}
}
&::-webkit-scrollbar {
width: 6px;
}
}
.refresh-action {
margin-left: auto;
margin-right: 6px;
&.active {
svg {
animation: RotateAnimation 0.5s linear infinite;
@keyframes RotateAnimation {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
}
}
}
.login-action {
display: flex;
flex-direction: row;

View File

@ -53,6 +53,7 @@ strong {
color: hsl(var(--text-secondary));
&:hover {
color: hsl(var(--text-secondary));
background: hsl(var(--accent-secondary));
}
}

View File

@ -48,11 +48,25 @@ div[data-radix-popper-content-wrapper=""] {
cursor: pointer;
}
.username {
color: hsl(var(--text));
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 120px;
&:before {
content: "@";
font-size: 12px;
margin-right: 1px;
color: hsl(var(--text-secondary));
}
}
.action-button {
width: 100%;
width: calc(100% - 4px);
cursor: pointer;
margin-top: 6px;
margin-bottom: 2px;
margin: 8px 2px 2px;
height: max-content !important;
}
}

View File

@ -0,0 +1,143 @@
import * as React from "react"
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
import { cn } from "./lib/utils"
import { buttonVariants } from "./button"
const AlertDialog = AlertDialogPrimitive.Root
const AlertDialogTrigger = AlertDialogPrimitive.Trigger
const AlertDialogPortal = ({
className,
...props
}: AlertDialogPrimitive.AlertDialogPortalProps) => (
<AlertDialogPrimitive.Portal className={cn(className)} {...props} />
)
AlertDialogPortal.displayName = AlertDialogPrimitive.Portal.displayName
const AlertDialogOverlay = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
>(({ className, children, ...props }, ref) => (
<AlertDialogPrimitive.Overlay
className={cn(
"fixed inset-0 z-50 bg-background/80 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
ref={ref}
/>
))
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
const AlertDialogContent = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
>(({ className, ...props }, ref) => (
<AlertDialogPortal>
<AlertDialogOverlay />
<AlertDialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg md:w-full",
className
)}
{...props}
/>
</AlertDialogPortal>
))
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
const AlertDialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-2 text-center sm:text-left",
className
)}
{...props}
/>
)
AlertDialogHeader.displayName = "AlertDialogHeader"
const AlertDialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
)
AlertDialogFooter.displayName = "AlertDialogFooter"
const AlertDialogTitle = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Title
ref={ref}
className={cn("text-lg font-semibold", className)}
{...props}
/>
))
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
const AlertDialogDescription = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
AlertDialogDescription.displayName =
AlertDialogPrimitive.Description.displayName
const AlertDialogAction = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Action>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Action
ref={ref}
className={cn(buttonVariants(), className)}
{...props}
/>
))
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
const AlertDialogCancel = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Cancel
ref={ref}
className={cn(
buttonVariants({ variant: "outline" }),
"mt-2 sm:mt-0",
className
)}
{...props}
/>
))
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
export {
AlertDialog,
AlertDialogTrigger,
AlertDialogContent,
AlertDialogHeader,
AlertDialogFooter,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogAction,
AlertDialogCancel,
}

View File

@ -1,6 +1,6 @@
import axios from "axios";
export const deploy: boolean = true;
export const deploy: boolean = false;
export let rest_api: string = "http://localhost:8094";
export let ws_api: string = "ws://localhost:8094";

View File

@ -0,0 +1,33 @@
import axios from "axios";
import type {ConversationInstance} from "../store/chat.ts";
import {removeHistory, setCurrent, setHistory, setMessages} from "../store/chat.ts";
export async function updateConversationList(dispatch: any): Promise<void> {
const resp = await axios.get("/conversation/list");
dispatch(setHistory(
resp.data.status ?
(resp.data.data || []) as ConversationInstance[]: []
));
}
export async function loadConversation(id: number): Promise<ConversationInstance> {
const resp = await axios.get(`/conversation/load?id=${id}`);
if (resp.data.status) return resp.data.data as ConversationInstance;
return { id, name: "" };
}
export async function deleteConversation(dispatch: any, id: number): Promise<boolean> {
const resp = await axios.get(`/conversation/delete?id=${id}`);
if (!resp.data.status) return false;
dispatch(removeHistory(id));
return true;
}
export async function toggleConversation(dispatch: any, id: number): Promise<ConversationInstance> {
const data = await loadConversation(id);
dispatch(setMessages(data));
dispatch(setCurrent(id));
return data;
}

View File

@ -1,7 +1,7 @@
import "../assets/home.less";
import {Input} from "../components/ui/input.tsx";
import {Toggle} from "../components/ui/toggle.tsx";
import {Globe, LogIn} from "lucide-react";
import {Globe, LogIn, MessageSquare, RotateCw, Trash2} from "lucide-react";
import {Button} from "../components/ui/button.tsx";
import {Switch} from "../components/ui/switch.tsx";
import {Label} from "../components/ui/label.tsx";
@ -11,26 +11,102 @@ import {
TooltipProvider,
TooltipTrigger,
} from "../components/ui/tooltip.tsx";
import {useSelector} from "react-redux";
import {useDispatch, useSelector} from "react-redux";
import type {RootState} from "../store";
import {selectAuthenticated} from "../store/auth.ts";
import {login} from "../conf.ts";
import {deleteConversation, updateConversationList} from "../conversation/history.ts";
import {useRef} from "react";
import {useAnimation, useEffectAsync} from "../utils.ts";
import {useToast} from "../components/ui/use-toast.ts";
import {ConversationInstance, selectHistory, setGPT4, setWeb} from "../store/chat.ts";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "../components/ui/alert-dialog.tsx";
function SideBar() {
const dispatch = useDispatch();
const open = useSelector((state: RootState) => state.menu.open);
const auth = useSelector(selectAuthenticated);
const { toast } = useToast();
const history: ConversationInstance[] = useSelector(selectHistory);
const refresh = useRef(null);
useEffectAsync(async () => {
await updateConversationList(dispatch);
}, []);
return (
<div className={`sidebar ${open ? "open" : ""}`}>
{
auth ?
<div className={`sidebar-content`}></div>
return (
<div className={`sidebar ${open ? "open" : ""}`}>
{
auth ?
<div className={`sidebar-content`}>
<Button
className={`refresh-action`} variant={`ghost`} size={`icon`} id={`refresh`}
ref={refresh} onClick={() => {
const hook = useAnimation(refresh, "active", 500);
updateConversationList(dispatch)
.catch(() => toast({
title: "Refresh failed",
description: "Failed to refresh conversations",
}))
.finally(hook);
}}>
<RotateCw className={`h-4 w-4`}/>
</Button>
<div className={`conversation-list`}>
{
history.map((conversation, i) => (
<div className={`conversation ${''}`} key={i}>
<MessageSquare className={`h-4 w-4 mr-1`}/>
<div className={`title`}>{conversation.name}</div>
<div className={`id`}>{conversation.id}</div>
<AlertDialog>
<AlertDialogTrigger asChild>
<Trash2 className={`delete h-4 w-4`} />
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
<AlertDialogDescription>
This action cannot be undone. This will permanently delete the conversation <strong>{conversation.name}</strong>.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={async () => {
if (await deleteConversation(dispatch, conversation.id)) toast({
title: "Conversation deleted",
description: `Conversation has been deleted.`,
})
else toast({
title: "Delete failed",
description: `Failed to delete conversation.`,
});
}}>
Delete
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
))
}
</div>
</div>
: <Button className={`login-action`} variant={`default`} onClick={login}>
<LogIn className={`h-3 w-3 mr-2`} /> login
<LogIn className={`h-3 w-3 mr-2`}/> login
</Button>
}
</div>
)
}
</div>
);
}
type ChatWrapperProps = {
@ -38,12 +114,16 @@ type ChatWrapperProps = {
}
function ChatWrapper({ onSend }: ChatWrapperProps) {
const dispatch = useDispatch();
const target = useRef(null);
function handleSend() {
const target = document.getElementById("input") as HTMLInputElement;
const message = target.value.trim();
if (!target.current) return;
const el = target.current as HTMLInputElement;
const message = el.value.trim();
if (message.length > 0) {
onSend?.(message);
target.value = "";
el.value = "";
}
}
@ -63,7 +143,9 @@ function ChatWrapper({ onSend }: ChatWrapperProps) {
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Toggle aria-label="Toggle chatgpt web feature" variant={`outline`}>
<Toggle aria-label="Toggle chatgpt web feature"
defaultPressed={true} onPressedChange={(state: boolean) => dispatch(setWeb(state))}
variant={`outline`}>
<Globe className="h-4 w-4" />
</Toggle>
</TooltipTrigger>
@ -72,14 +154,14 @@ function ChatWrapper({ onSend }: ChatWrapperProps) {
</TooltipContent>
</Tooltip>
</TooltipProvider>
<Input className={`input-box`} id={`input`} placeholder={`Write something...`} />
<Input className={`input-box`} ref={target} placeholder={`Write something...`} />
<Button size={`icon`} variant="outline" className={`send-button`} onClick={handleSend}>
<svg className="h-4 w-4" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" data-v-f9a7276b=""><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`}>
<div className="flex items-center space-x-2">
<Switch id="enable-gpt4" />
<Switch id="enable-gpt4" onCheckedChange={(state: boolean) => dispatch(setGPT4(state))} />
<Label htmlFor="enable-gpt4">GPT-4</Label>
</div>
</div>

View File

@ -6,28 +6,50 @@ type Message = {
isBot: boolean;
}
export type ConversationInstance = {
id: number;
name: string;
message?: {
content: string;
role: string;
}
}
type initialStateType = {
history: ConversationInstance[];
messages: Message[];
gpt4: boolean;
web: boolean;
current: number;
}
const chatSlice = createSlice({
name: 'chat',
initialState: {
history: [],
messages: [],
gpt4: false,
web: false,
web: true,
current: -1,
} as initialStateType,
reducers: {
setHistory: (state, action) => {
state.history = action.payload as ConversationInstance[];
},
removeHistory: (state, action) => {
state.history = state.history.filter((item) => item.id !== (action.payload as number));
},
setMessages: (state, action) => {
state.messages = action.payload;
state.messages = action.payload as Message[];
},
setGPT4: (state, action) => {
state.gpt4 = action.payload;
state.gpt4 = action.payload as boolean;
},
setWeb: (state, action) => {
state.web = action.payload;
state.web = action.payload as boolean;
},
setCurrent: (state, action) => {
state.current = action.payload as number;
},
addMessage: (state, action) => {
state.messages.push(action.payload as Message);
@ -38,5 +60,11 @@ const chatSlice = createSlice({
}
});
export const {setMessages, addMessage, setMessage, setGPT4, setWeb} = chatSlice.actions;
export const {setHistory, removeHistory, setCurrent, setMessages, setGPT4, setWeb, addMessage, setMessage} = chatSlice.actions;
export const selectHistory = (state: any) => state.chat.history;
export const selectMessages = (state: any) => state.chat.messages;
export const selectGPT4 = (state: any) => state.chat.gpt4;
export const selectWeb = (state: any) => state.chat.web;
export const selectCurrent = (state: any) => state.chat.current;
export default chatSlice.reducer;

View File

@ -1,11 +1,13 @@
import { configureStore } from '@reduxjs/toolkit'
import menuReducer from './menu'
import authReducer from './auth'
import chatReducer from './chat'
const store = configureStore({
reducer: {
menu: menuReducer,
auth: authReducer,
chat: chatReducer,
},
});

View File

@ -1,9 +1,10 @@
import {createSlice} from "@reduxjs/toolkit";
import {mobile} from "../utils.ts";
export const menuSlice = createSlice({
name: 'menu',
initialState: {
open: false,
open: !mobile, // mobile: false, desktop: true
},
reducers: {
toggleMenu: (state) => {

27
renio/src/utils.ts Normal file
View File

@ -0,0 +1,27 @@
import React, {useEffect} from "react";
export let mobile = (window.innerWidth <= 468 || window.innerHeight <= 468 || navigator.userAgent.includes("Mobile"));
export function useEffectAsync(effect: () => Promise<any>, deps?: any[]) {
return useEffect(() => {
effect()
.catch((err) => console.debug("[runtime] error during use effect", err));
}, deps);
}
export function useAnimation(ref: React.MutableRefObject<any>, cls: string, min?: number): (() => number) | undefined {
if (!ref.current) return;
const target = ref.current as HTMLButtonElement;
const stamp = Date.now();
target.classList.add(cls);
return function () {
const duration = Date.now() - stamp;
const timeout = min ? Math.max(min - duration, 0) : 0;
setTimeout(() => target.classList.remove(cls), timeout);
return timeout;
}
}
window.addEventListener("resize", () => {
mobile = (window.innerWidth <= 468 || window.innerHeight <= 468 || navigator.userAgent.includes("Mobile"));
});