mirror of
https://github.com/coaidev/coai.git
synced 2025-05-23 06:50:14 +09:00
update history and chat storage
This commit is contained in:
parent
36ea2ba04e
commit
4fbc8f24b1
@ -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
63
renio/pnpm-lock.yaml
generated
@ -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
29
renio/qodana.yaml
Normal 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
|
@ -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
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -53,6 +53,7 @@ strong {
|
||||
color: hsl(var(--text-secondary));
|
||||
|
||||
&:hover {
|
||||
color: hsl(var(--text-secondary));
|
||||
background: hsl(var(--accent-secondary));
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
143
renio/src/components/ui/alert-dialog.tsx
Normal file
143
renio/src/components/ui/alert-dialog.tsx
Normal 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,
|
||||
}
|
@ -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";
|
||||
|
||||
|
33
renio/src/conversation/history.ts
Normal file
33
renio/src/conversation/history.ts
Normal 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;
|
||||
}
|
@ -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>
|
||||
|
@ -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;
|
||||
|
@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -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
27
renio/src/utils.ts
Normal 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"));
|
||||
});
|
Loading…
Reference in New Issue
Block a user