host of Quality of life fixes
This commit is contained in:
@@ -20,6 +20,7 @@ import Account2 from './llm-fe/pages/Account2/Account2';
|
|||||||
import AnalyticsPage from './llm-fe/pages/Analytics/Analytics';
|
import AnalyticsPage from './llm-fe/pages/Analytics/Analytics';
|
||||||
import PasswordResetConfirmation from './llm-fe/pages/PasswordResetConfirmation/PasswordReset';
|
import PasswordResetConfirmation from './llm-fe/pages/PasswordResetConfirmation/PasswordReset';
|
||||||
import GlobalThemeWrapper from './llm-fe/components/GlobalThemeWrapper/GlobalThemeWrapper';
|
import GlobalThemeWrapper from './llm-fe/components/GlobalThemeWrapper/GlobalThemeWrapper';
|
||||||
|
import Tracker from './llm-fe/components/Tracker/Tracker';
|
||||||
|
|
||||||
const ProtectedRoutes = () => {
|
const ProtectedRoutes = () => {
|
||||||
const { authenticated, needsNewPassword, loading } = useContext(AuthContext);
|
const { authenticated, needsNewPassword, loading } = useContext(AuthContext);
|
||||||
@@ -34,11 +35,14 @@ const ProtectedRoutes = () => {
|
|||||||
return <Outlet key={Date.now()} />
|
return <Outlet key={Date.now()} />
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class App extends Component {
|
class App extends Component {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<GlobalThemeWrapper>
|
<GlobalThemeWrapper>
|
||||||
|
<Tracker />
|
||||||
<div className='site'>
|
<div className='site'>
|
||||||
<main>
|
<main>
|
||||||
<div className="main-container">
|
<div className="main-container">
|
||||||
|
|||||||
23
llm-fe/src/llm-fe/components/Tracker/Tracker.tsx
Normal file
23
llm-fe/src/llm-fe/components/Tracker/Tracker.tsx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import React, { useEffect } from 'react';
|
||||||
|
|
||||||
|
const Tracker: React.FC = () => {
|
||||||
|
useEffect(() => {
|
||||||
|
if (process.env.NODE_ENV === 'production') {
|
||||||
|
const script = document.createElement('script');
|
||||||
|
script.src = "https://tianji.aimloperations.com/tracker.js";
|
||||||
|
script.async = true;
|
||||||
|
script.defer = true;
|
||||||
|
script.setAttribute('data-website-id', 'cm7x7m52m03kbddswbswrt17y');
|
||||||
|
|
||||||
|
document.body.appendChild(script);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.body.removeChild(script);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Tracker;
|
||||||
@@ -18,6 +18,8 @@ function WebSocketProvider({ children }) {
|
|||||||
const channels = useRef({}); // maps each channel to the callback
|
const channels = useRef({}); // maps each channel to the callback
|
||||||
const { account, setAccount } = useContext(AccountContext);
|
const { account, setAccount } = useContext(AccountContext);
|
||||||
const [currentChannel, setCurrentChannel] = useState("");
|
const [currentChannel, setCurrentChannel] = useState("");
|
||||||
|
const [isConnected, setIsConnected] = useState(false);
|
||||||
|
|
||||||
/* called from a component that registers a callback for a channel */
|
/* called from a component that registers a callback for a channel */
|
||||||
const subscribe = (channel, callback) => {
|
const subscribe = (channel, callback) => {
|
||||||
//console.log(`Subbing to ${channel}`)
|
//console.log(`Subbing to ${channel}`)
|
||||||
@@ -76,8 +78,12 @@ function WebSocketProvider({ children }) {
|
|||||||
|
|
||||||
ws.current.onopen = () => {
|
ws.current.onopen = () => {
|
||||||
setSocket(ws.current);
|
setSocket(ws.current);
|
||||||
|
setIsConnected(true);
|
||||||
|
};
|
||||||
|
ws.current.onclose = () => {
|
||||||
|
console.log('websocket closed');
|
||||||
|
setIsConnected(false);
|
||||||
};
|
};
|
||||||
ws.current.onclose = () => { };
|
|
||||||
ws.current.onmessage = (message) => {
|
ws.current.onmessage = (message) => {
|
||||||
const data = message.data;
|
const data = message.data;
|
||||||
// lookup for an existing chat in which this message belongs
|
// lookup for an existing chat in which this message belongs
|
||||||
@@ -103,7 +109,7 @@ function WebSocketProvider({ children }) {
|
|||||||
/* subscribe and unsubscribe are the only required prop for the context */
|
/* subscribe and unsubscribe are the only required prop for the context */
|
||||||
return (
|
return (
|
||||||
<WebSocketContext.Provider
|
<WebSocketContext.Provider
|
||||||
value={[subscribe, unsubscribe, socket, sendMessage]}
|
value={[subscribe, unsubscribe, socket, sendMessage, isConnected]}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</WebSocketContext.Provider>
|
</WebSocketContext.Provider>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import styled, { ThemeContext } from "styled-components";
|
|||||||
import { Formik, Form, Field, ErrorMessage } from "formik";
|
import { Formik, Form, Field, ErrorMessage } from "formik";
|
||||||
import * as Yup from "yup";
|
import * as Yup from "yup";
|
||||||
import { AttachFile, Delete, Send } from "@mui/icons-material"; // Keeping icons for now, can replace later if needed
|
import { AttachFile, Delete, Send } from "@mui/icons-material"; // Keeping icons for now, can replace later if needed
|
||||||
|
import { Tooltip } from "@mui/material";
|
||||||
import Markdown from "markdown-to-jsx";
|
import Markdown from "markdown-to-jsx";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -110,7 +111,7 @@ const StyledInputContainer = styled.div`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledInput = styled.input`
|
const StyledInput = styled.textarea`
|
||||||
flex: 1;
|
flex: 1;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: none;
|
border: none;
|
||||||
@@ -118,6 +119,11 @@ const StyledInput = styled.input`
|
|||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
padding: 0.8rem;
|
padding: 0.8rem;
|
||||||
outline: none;
|
outline: none;
|
||||||
|
resize: none;
|
||||||
|
overflow-y: auto;
|
||||||
|
max-height: 150px; /* Approx 5 lines */
|
||||||
|
font-family: inherit;
|
||||||
|
line-height: 1.5;
|
||||||
|
|
||||||
&::placeholder {
|
&::placeholder {
|
||||||
color: ${({ theme }) => theme.darkMode ? 'rgba(255, 255, 255, 0.4)' : 'rgba(0, 0, 0, 0.4)'};
|
color: ${({ theme }) => theme.darkMode ? 'rgba(255, 255, 255, 0.4)' : 'rgba(0, 0, 0, 0.4)'};
|
||||||
@@ -147,6 +153,23 @@ const IconButton = styled.button`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const StyledSelect = styled.select`
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
color: ${({ theme }) => theme.colors.text};
|
||||||
|
font-size: 0.9rem;
|
||||||
|
padding: 0.5rem;
|
||||||
|
outline: none;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
border-right: 1px solid ${({ theme }) => theme.colors.cardBorder};
|
||||||
|
|
||||||
|
option {
|
||||||
|
background: ${({ theme }) => theme.colors.background || '#1a1a1a'};
|
||||||
|
color: ${({ theme }) => theme.colors.text};
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
const ConversationItem = styled.div<{ $active: boolean }>`
|
const ConversationItem = styled.div<{ $active: boolean }>`
|
||||||
padding: 0.8rem 1rem;
|
padding: 0.8rem 1rem;
|
||||||
margin-bottom: 0.5rem;
|
margin-bottom: 0.5rem;
|
||||||
@@ -226,7 +249,7 @@ const AlwaysScrollToBottom = (): JSX.Element => {
|
|||||||
|
|
||||||
const AsyncDashboardInner = ({ }): JSX.Element => {
|
const AsyncDashboardInner = ({ }): JSX.Element => {
|
||||||
const [announcements, setAnnouncement] = useState<Announcement[]>([]);
|
const [announcements, setAnnouncement] = useState<Announcement[]>([]);
|
||||||
const [subscribe, unsubscribe, socket, sendMessage] =
|
const [subscribe, unsubscribe, socket, sendMessage, isConnected] =
|
||||||
useContext(WebSocketContext);
|
useContext(WebSocketContext);
|
||||||
const { account } = useContext(AccountContext);
|
const { account } = useContext(AccountContext);
|
||||||
|
|
||||||
@@ -242,6 +265,7 @@ const AsyncDashboardInner = ({ }): JSX.Element => {
|
|||||||
|
|
||||||
const conversationRef = useRef(conversationDetails);
|
const conversationRef = useRef(conversationDetails);
|
||||||
const theme = useContext(ThemeContext);
|
const theme = useContext(ThemeContext);
|
||||||
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||||
|
|
||||||
async function GetAnnouncements() {
|
async function GetAnnouncements() {
|
||||||
const response: AxiosResponse<AnnouncementType[]> =
|
const response: AxiosResponse<AnnouncementType[]> =
|
||||||
@@ -271,12 +295,32 @@ const AsyncDashboardInner = ({ }): JSX.Element => {
|
|||||||
conversationRef.current = tempConversations;
|
conversationRef.current = tempConversations;
|
||||||
setConversationDetails(tempConversations);
|
setConversationDetails(tempConversations);
|
||||||
sendMessage(prompt, selectedConversation, file, fileType, modelName);
|
sendMessage(prompt, selectedConversation, file, fileType, modelName);
|
||||||
resetForm();
|
resetForm({
|
||||||
|
values: {
|
||||||
|
prompt: "",
|
||||||
|
file: null,
|
||||||
|
fileType: null,
|
||||||
|
modelName: modelName, // Keep the selected model
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reset textarea height
|
||||||
|
if (textareaRef.current) {
|
||||||
|
textareaRef.current.style.height = 'auto';
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log(`error ${e}`);
|
console.log(`error ${e}`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const adjustHeight = () => {
|
||||||
|
const textarea = textareaRef.current;
|
||||||
|
if (textarea) {
|
||||||
|
textarea.style.height = 'auto';
|
||||||
|
textarea.style.height = `${textarea.scrollHeight}px`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContainer>
|
<PageContainer>
|
||||||
<ParticleBackground />
|
<ParticleBackground />
|
||||||
@@ -350,13 +394,24 @@ const AsyncDashboardInner = ({ }): JSX.Element => {
|
|||||||
prompt: "",
|
prompt: "",
|
||||||
file: null,
|
file: null,
|
||||||
fileType: null,
|
fileType: null,
|
||||||
modelName: "",
|
modelName: "FAST",
|
||||||
}}
|
}}
|
||||||
validationSchema={validationSchema}
|
validationSchema={validationSchema}
|
||||||
onSubmit={handlePromptSubmit}
|
onSubmit={handlePromptSubmit}
|
||||||
>
|
>
|
||||||
{(formik) => (
|
{(formik) => (
|
||||||
<Form>
|
<Form>
|
||||||
|
{!isConnected && (
|
||||||
|
<div style={{
|
||||||
|
color: '#ff4444',
|
||||||
|
textAlign: 'center',
|
||||||
|
marginBottom: '0.5rem',
|
||||||
|
fontSize: '0.9rem',
|
||||||
|
fontWeight: 500
|
||||||
|
}}>
|
||||||
|
Connection to the LLM is not active
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<StyledInputContainer>
|
<StyledInputContainer>
|
||||||
<label htmlFor="file-upload" style={{ cursor: 'pointer', display: 'flex' }}>
|
<label htmlFor="file-upload" style={{ cursor: 'pointer', display: 'flex' }}>
|
||||||
<IconButton as="span">
|
<IconButton as="span">
|
||||||
@@ -380,19 +435,50 @@ const AsyncDashboardInner = ({ }): JSX.Element => {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<Field
|
<Field
|
||||||
name="prompt"
|
as={StyledSelect}
|
||||||
as={StyledInput}
|
name="modelName"
|
||||||
placeholder="Type a message..."
|
|
||||||
autoComplete="off"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<IconButton
|
|
||||||
type="submit"
|
|
||||||
disabled={!formik.isValid || !formik.dirty}
|
|
||||||
style={{ color: formik.isValid && formik.dirty ? '#fff' : undefined, background: formik.isValid && formik.dirty ? theme?.main : undefined }}
|
|
||||||
>
|
>
|
||||||
<Send />
|
<option value="FAST">FAST</option>
|
||||||
</IconButton>
|
<option value="THINKING">THINKING</option>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field name="prompt">
|
||||||
|
{({ field }: any) => (
|
||||||
|
<StyledInput
|
||||||
|
{...field}
|
||||||
|
ref={textareaRef}
|
||||||
|
placeholder="Type a message..."
|
||||||
|
rows={1}
|
||||||
|
onKeyDown={(e: React.KeyboardEvent) => {
|
||||||
|
if (e.key === 'Enter' && !e.shiftKey) {
|
||||||
|
e.preventDefault();
|
||||||
|
if (formik.isValid && formik.dirty) {
|
||||||
|
formik.submitForm();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
|
field.onChange(e);
|
||||||
|
adjustHeight();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Tooltip title={!isConnected ? "Connection to the LLM is not active" : "Send message"}>
|
||||||
|
<span>
|
||||||
|
<IconButton
|
||||||
|
type="submit"
|
||||||
|
disabled={!formik.isValid || !formik.dirty || !isConnected}
|
||||||
|
style={{
|
||||||
|
color: (formik.isValid && formik.dirty && isConnected) ? '#fff' : undefined,
|
||||||
|
background: (formik.isValid && formik.dirty && isConnected) ? theme?.main : undefined
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Send />
|
||||||
|
</IconButton>
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
</StyledInputContainer>
|
</StyledInputContainer>
|
||||||
{formik.values.file && (
|
{formik.values.file && (
|
||||||
<div style={{
|
<div style={{
|
||||||
|
|||||||
Reference in New Issue
Block a user