diff --git a/renio/package.json b/renio/package.json index 33f8c6c..9928029 100644 --- a/renio/package.json +++ b/renio/package.json @@ -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", diff --git a/renio/pnpm-lock.yaml b/renio/pnpm-lock.yaml index e9318be..10dea79 100644 --- a/renio/pnpm-lock.yaml +++ b/renio/pnpm-lock.yaml @@ -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: diff --git a/renio/qodana.yaml b/renio/qodana.yaml new file mode 100644 index 0000000..29f8f8c --- /dev/null +++ b/renio/qodana.yaml @@ -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: + +#Disable inspections +#exclude: +# - name: +# paths: +# - + +#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 can be found at https://plugins.jetbrains.com) + +#Specify Qodana linter for analysis (Applied in CI/CD pipeline) +linter: jetbrains/qodana-js:latest diff --git a/renio/src/App.tsx b/renio/src/App.tsx index e08666c..89c845a 100644 --- a/renio/src/App.tsx +++ b/renio/src/App.tsx @@ -35,9 +35,10 @@ function Settings() { - My Account + { username } Quota + +
+ { + history.map((conversation, i) => ( +
+ +
{conversation.name}
+
{conversation.id}
+ + + + + + + Are you absolutely sure? + + This action cannot be undone. This will permanently delete the conversation {conversation.name}. + + + + Cancel + { + 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 + + + + +
+ )) + } +
+ : - } - - ) + } + + ); } 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) { - + dispatch(setWeb(state))} + variant={`outline`}> @@ -72,14 +154,14 @@ function ChatWrapper({ onSend }: ChatWrapperProps) { - +
- + dispatch(setGPT4(state))} />
diff --git a/renio/src/store/chat.ts b/renio/src/store/chat.ts index a153e77..75698ee 100644 --- a/renio/src/store/chat.ts +++ b/renio/src/store/chat.ts @@ -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; diff --git a/renio/src/store/index.ts b/renio/src/store/index.ts index 4412b7c..4d1600d 100644 --- a/renio/src/store/index.ts +++ b/renio/src/store/index.ts @@ -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, }, }); diff --git a/renio/src/store/menu.ts b/renio/src/store/menu.ts index 187c0c3..d0c674d 100644 --- a/renio/src/store/menu.ts +++ b/renio/src/store/menu.ts @@ -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) => { diff --git a/renio/src/utils.ts b/renio/src/utils.ts new file mode 100644 index 0000000..09fdf3c --- /dev/null +++ b/renio/src/utils.ts @@ -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, deps?: any[]) { + return useEffect(() => { + effect() + .catch((err) => console.debug("[runtime] error during use effect", err)); + }, deps); +} + +export function useAnimation(ref: React.MutableRefObject, 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")); +});