Compare commits

..
1 Commits
Author SHA1 Message Date
westfarn a0a034bbf8 Wire Tianji custom event tracking for auth, billing, and chat (#36)
Unit Tests / test (pull_request) Successful in 12s
Add identifyAccount + AnalyticsSession, instrument MVP funnel call sites without PII payloads, and document event names in ANALYTICS.md. Billing portal event reserved until #33.
2026-07-27 09:05:37 -05:00
9 changed files with 90 additions and 326 deletions
+3 -4
View File
@@ -10,7 +10,6 @@ import PasswordReset from './llm-fe/pages/PasswordReset/PasswordReset';
import NotFound from './llm-fe/pages/NotFound/NotFound';
import { AuthContext } from './llm-fe/contexts/AuthContext';
import TermsOfService from './llm-fe/pages/TermsOfService/TermsOfService';
import PublicTermsOfServicePage from './llm-fe/pages/TermsOfService/PublicTermsOfServicePage';
import SetPassword from './llm-fe/components/SetPassword/SetPassword';
import AsyncDashboard2 from './llm-fe/pages/AsyncDashboard2/AsyncDashboard2';
import FeedbackPage2 from './llm-fe/pages/FeedbackPage2/FeedbackPage2';
@@ -64,14 +63,14 @@ class App extends Component {
<Route path={"/password_reset/"} Component={PasswordReset} />
<Route path={"/password_reset_confirmation/"} Component={PasswordResetConfirmation} />
<Route path={'/set_password/'} Component={SetPassword} />
{/* Public legal pages (also used for post-login ToS acknowledge) */}
<Route path={"/terms_of_service/"} Component={TermsOfService} />
<Route path={"/legal/terms-of-service"} Component={PublicTermsOfServicePage} />
<Route element={<ProtectedRoutes />}>
<Route path={"/"} index={true} Component={AsyncDashboard2} />
<Route path={"/account/"} Component={Account2} />
<Route path={"/document_storage"} Component={DocumentStoragePage} />
<Route path={"/terms_of_service/"} Component={TermsOfService} />
<Route path={"/feedback/"} Component={FeedbackPage2} />
<Route path={"/analytics/"} Component={AnalyticsPage} />
@@ -1,5 +1,4 @@
import React from 'react';
import { MemoryRouter } from 'react-router-dom';
import { render, screen } from '@testing-library/react';
import Footer from './Footer';
@@ -9,21 +8,13 @@ jest.mock('../../ui-kit/components/MDTypography', () => ({
}));
describe('Footer', () => {
it('renders copyright, developer credit, and Terms link', () => {
render(
<MemoryRouter>
<Footer />
</MemoryRouter>,
);
it('renders copyright and developer credit', () => {
render(<Footer />);
expect(screen.getByText(/© 2026 Hesychia/i)).toBeInTheDocument();
expect(screen.getByRole('link', { name: /AI ML Operations, LLC/i })).toHaveAttribute(
'href',
'https://www.aimloperations.com',
);
expect(screen.getByRole('link', { name: /Terms of Service/i })).toHaveAttribute(
'href',
'/terms_of_service/',
'https://www.aimloperations.com'
);
});
});
@@ -1,17 +1,13 @@
import React from 'react';
import { Link as RouterLink } from 'react-router-dom';
import MDTypography from '../../ui-kit/components/MDTypography';
const Footer = (): JSX.Element => {
return (
<div className="continer-fluid">
<MDTypography>
&copy; 2026 Hesychia | Developed by{' '}
<a href="https://www.aimloperations.com">AI ML Operations, LLC</a>
{' · '}
<RouterLink to="/terms_of_service/">Terms of Service</RouterLink>
</MDTypography>
<div className='continer-fluid'>
<MDTypography>&copy; 2026 Hesychia | Developed by <a href='https://www.aimloperations.com'>AI ML Operations, LLC</a></MDTypography>
</div>
);
};
@@ -1,138 +0,0 @@
import React, { Fragment } from 'react';
import { Typography } from '@mui/material';
/** Shared legal text for in-app ToS acknowledge and public read-only page. */
const TermsOfServiceDocument = (): JSX.Element => (
<Fragment>
<Typography variant="caption" color="text.secondary" display="block" sx={{ mb: 1 }}>
Last Updated: July 27, 2026
</Typography>
<Typography variant="body1" paragraph>
Welcome to Hesychia (hesychia.ai)! These Terms of Service (&quot;Terms&quot;) are a legally binding
agreement between you and AI ML Operations, LLC (&quot;we,&quot; &quot;us,&quot; or &quot;our&quot;) governing your access
to and use of the Hesychia large language model chat service (the &quot;Service&quot;).
</Typography>
<Typography variant="body1" paragraph>
<strong>Please read these Terms carefully.</strong> By accessing or using the Service, you agree
to be bound by these Terms. If you do not agree, do not use the Service.
</Typography>
<Typography variant="h5" component="h2" gutterBottom id="acceptance">
1. Acceptance of Terms
</Typography>
<Typography variant="body1" paragraph>
By creating an account, signing in, or otherwise using the Service, you acknowledge that you have
read, understood, and agree to be bound by these Terms. If you are using the Service on behalf of
an organization, you represent that you have authority to bind that organization.
</Typography>
<Typography variant="h5" component="h2" gutterBottom id="data-confidentiality">
2. Data Confidentiality and Privacy
</Typography>
<Typography variant="body1" paragraph>
We respect your data. Information you enter into the Service is contained within your organization
workspace and will not be sold, shared, or disclosed to third parties without your explicit consent,
except as needed to operate the Service (for example, infrastructure providers under appropriate
agreements) or as required by law. See also Section 6 (Analytics and Cookies).
</Typography>
<Typography variant="h5" component="h2" gutterBottom id="acceptable-use">
3. Acceptable Use
</Typography>
<Typography variant="body1" paragraph>
You agree to use the Service for legitimate purposes only and to comply with all applicable laws
and regulations. You must not use the Service to harass others, distribute malware, attempt
unauthorized access, scrape or overload the Service, or generate content that violates law or
third-party rights.
</Typography>
<Typography variant="h5" component="h2" gutterBottom id="intellectual-property">
4. Intellectual Property
</Typography>
<Typography variant="body1" paragraph>
All intellectual property rights in the Service, including the software and models we provide,
remain the property of AI ML Operations, LLC or its licensors. Output you generate through the
Service is for your use; AI ML Operations, LLC makes no claim of ownership over that output,
subject to any rights of underlying model providers and applicable law.
</Typography>
<Typography variant="h5" component="h2" gutterBottom id="ai-disclaimer">
5. Limitation of Liability and AI Disclaimer
</Typography>
<Typography variant="body1" paragraph>
The Service provides AI-generated responses that may be inaccurate, incomplete, or inappropriate
for your use case. You are solely responsible for reviewing outputs before relying on them.
<strong>
{' '}
The Service is provided &quot;AS IS&quot; and &quot;AS AVAILABLE&quot; without warranties of any kind.
</strong>{' '}
To the fullest extent permitted by law, AI ML Operations, LLC is not liable for any damages,
including consequential, incidental, or punitive damages, arising from your use of the Service
or reliance on any AI-generated content.
</Typography>
<Typography variant="h5" component="h2" gutterBottom id="analytics">
6. Analytics and Cookies
</Typography>
<Typography variant="body1" paragraph>
We use privacy-focused analytics (Tianji, operated on our infrastructure) to understand how the
Service is used, diagnose issues, and improve features. We do not use advertising cookies. Before
optional behavioral analytics run on production or beta builds, we ask for your consent through a
notice on the Service. You may accept or decline at that time.
</Typography>
<Typography variant="body1" component="div" paragraph>
We always collect limited usage information such as:
<ul>
<li>Anonymous page views and general device or browser type</li>
<li>Referral or campaign parameters (such as UTM tags) and referrer information when present</li>
</ul>
If you accept optional analytics, we may also collect:
<ul>
<li>
Product actions (for example, sign-in, sign-up, checkout, conversation created, message sent
without message or document contents)
</li>
<li>
When you are signed in, a stable account identifier (such as email) so we can understand
authenticated usage
</li>
</ul>
</Typography>
<Typography variant="body1" paragraph>
If you decline optional analytics, we still collect anonymous page views and referral data
described above, but we do not collect optional product events or associate activity with your
account. Strictly necessary storage used for security and core functionality (such as JWT
authentication in local storage) is separate from optional analytics. You can change your
analytics choice by clearing site data for Hesychia in your browser; the consent notice will
appear again on your next visit.
</Typography>
<Typography variant="h5" component="h2" gutterBottom id="changes">
7. Changes to Terms
</Typography>
<Typography variant="body1" paragraph>
We may update these Terms from time to time. We will notify you of significant changes, and your
continued use of the Service after changes take effect constitutes acceptance of the updated Terms.
</Typography>
<Typography variant="h5" component="h2" gutterBottom id="governing-law">
8. Governing Law
</Typography>
<Typography variant="body1" paragraph>
These Terms are governed by and construed in accordance with the laws of the United States and
the State of Illinois, without regard to conflict-of-law principles.
</Typography>
<Typography variant="h5" component="h2" gutterBottom id="contact">
9. Contact
</Typography>
<Typography variant="body1" paragraph>
Questions about these Terms? Contact AI ML Operations, LLC via{' '}
<a href="https://www.aimloperations.com">aimloperations.com</a> or your Hesychia account
administrator.
</Typography>
</Fragment>
);
export default TermsOfServiceDocument;
@@ -278,9 +278,6 @@ const SignIn = (): JSX.Element => {
Don't have an account? Sign up
</SignUpLink>
)}
<SignUpLink onClick={() => navigate('/terms_of_service/')}>
Terms of Service
</SignUpLink>
</GlassCard>
</ContentWrapper>
</PageContainer>
@@ -361,9 +361,6 @@ const SignUp = (): JSX.Element => {
)}
</Formik>
<NavLink to="/signin/">Already have an account? Sign in</NavLink>
<NavLink to="/terms_of_service/" style={{ marginTop: '0.75rem' }}>
Terms of Service
</NavLink>
</GlassCard>
</ContentWrapper>
</PageContainer>
@@ -1,7 +0,0 @@
import React from 'react';
import TermsOfService from './TermsOfService';
/** Unauthenticated / marketing entry for Terms of Service. */
const PublicTermsOfServicePage = (): JSX.Element => <TermsOfService publicView />;
export default PublicTermsOfServicePage;
@@ -1,48 +0,0 @@
import React from 'react';
import { MemoryRouter } from 'react-router-dom';
import { render, screen } from '@testing-library/react';
import { AuthContext } from '../../contexts/AuthContext';
import { AccountContext } from '../../contexts/AccountContext';
import PublicTermsOfServicePage from './PublicTermsOfServicePage';
jest.mock('../../../axiosApi', () => ({
axiosInstance: {
post: jest.fn(),
get: jest.fn(),
defaults: { headers: { common: {} as Record<string, string | null> } },
},
}));
jest.mock('../../components/PageWrapperLayout/PageWrapperLayout', () => ({
__esModule: true,
default: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
}));
describe('PublicTermsOfServicePage', () => {
it('renders key headings without auth', () => {
render(
<MemoryRouter>
<AuthContext.Provider
value={{
authenticated: false,
loading: false,
setAuthentication: jest.fn(),
needsNewPassword: false,
setNeedsNewPassword: jest.fn(),
}}
>
<AccountContext.Provider value={{ account: undefined, setAccount: jest.fn() }}>
<PublicTermsOfServicePage />
</AccountContext.Provider>
</AuthContext.Provider>
</MemoryRouter>,
);
expect(screen.getByRole('heading', { name: /Terms of Service/i })).toBeInTheDocument();
expect(screen.getByRole('heading', { name: /Acceptance of Terms/i })).toBeInTheDocument();
expect(screen.getByRole('heading', { name: /Analytics and Cookies/i })).toBeInTheDocument();
expect(screen.getByRole('heading', { name: /Governing Law/i })).toBeInTheDocument();
expect(screen.getByRole('heading', { name: /Contact/i })).toBeInTheDocument();
expect(screen.queryByRole('button', { name: /Acknowledge/i })).not.toBeInTheDocument();
});
});
@@ -1,115 +1,92 @@
import React, { useContext, useState } from 'react';
import { Alert, Box, Button, Card, CardContent, Divider, Stack, Typography } from '@mui/material';
import { Link as RouterLink, useNavigate } from 'react-router-dom';
import { Form, Formik } from 'formik';
import React from 'react';
import { Card, CardContent, Divider } from '@mui/material';
import { axiosInstance } from '../../../axiosApi';
import { AuthContext } from '../../contexts/AuthContext';
import { AccountContext } from '../../contexts/AccountContext';
import { Account } from '../../data';
import TermsOfServiceDocument from '../../components/legal/TermsOfServiceDocument';
import { useNavigate } from 'react-router-dom';
import MDTypography from '../../ui-kit/components/MDTypography';
import PageWrapperLayout from '../../components/PageWrapperLayout/PageWrapperLayout';
import MDBox from '../../ui-kit/components/MDBox';
import MDButton from '../../ui-kit/components/MDButton';
import { AnalyticsEvents, trackEvent } from '../../utils/analytics';
type TermsOfServiceProps = {
/** Marketing / legal alias: always read-only chrome (no app shell). */
publicView?: boolean;
};
/**
* Shared Terms page: public without auth; Acknowledge when signed in on /terms_of_service/.
*/
const TermsOfService = ({ publicView = false }: TermsOfServiceProps): JSX.Element => {
const TermsOfService = (): JSX.Element => {
const navigate = useNavigate();
const { authenticated } = useContext(AuthContext);
const { account, setAccount } = useContext(AccountContext);
const [error, setError] = useState<string | null>(null);
const [submitting, setSubmitting] = useState(false);
const showAcknowledge = authenticated && !publicView;
const handleAcknowledge = async (): Promise<void> => {
setError(null);
setSubmitting(true);
try{
await axiosInstance.post('/user/acknowledge_tos/');
await axiosInstance.post('/user/acknowledge_tos/')
trackEvent(AnalyticsEvents.TOS_ACKNOWLEDGED);
if (account) {
setAccount(new Account({ ...account, has_signed_tos: true }));
}
navigate('/');
} catch (err) {
console.log(err);
setError('Unable to acknowledge. Please try again.');
} finally {
setSubmitting(false);
}
};
navigate('/')
const documentBody = (
<Card sx={{ maxWidth: 900, mx: 'auto', width: '100%' }}>
<CardContent>
<Typography variant="h3" component="h1" gutterBottom>
Terms of Service
</Typography>
<Typography variant="subtitle1" color="text.secondary" gutterBottom>
Hesychia · hesychia.ai · AI ML Operations, LLC
</Typography>
</CardContent>
<Divider />
<CardContent>
<Stack spacing={1.5}>
<TermsOfServiceDocument />
</Stack>
</CardContent>
{showAcknowledge && (
<>
<Divider />
<CardContent>
{error && (
<Alert severity="error" sx={{ mb: 2 }}>
{error}
</Alert>
)}
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
By clicking Acknowledge, you confirm that you have read and agree to these Terms.
</Typography>
<Button
variant="contained"
onClick={handleAcknowledge}
disabled={submitting}
fullWidth
>
{submitting ? 'Saving…' : 'Acknowledge & Agree'}
</Button>
</CardContent>
</>
)}
{!authenticated && (
<>
<Divider />
<CardContent>
<Typography variant="body2" color="text.secondary">
Already have an account?{' '}
<Button component={RouterLink} to="/signin/" size="small">
Sign in
</Button>
</Typography>
</CardContent>
</>
)}
</Card>
);
if (publicView || !authenticated) {
return (
<Box sx={{ py: 4, px: 2, minHeight: '100vh', bgcolor: 'background.default' }}>
{documentBody}
</Box>
);
}catch(error){
console.log(error)
}
}
return (
<PageWrapperLayout>
<Box sx={{ margin: '0 auto', width: { xs: '95%', md: '80%' }, py: 2 }}>{documentBody}</Box>
<MDBox sx={{ margin: '0 auto', width: '80%', height: '80%', minHeight: '80%', maxHeight: '80%', align:'center'}}>
<Card>
<CardContent>
<MDTypography variant="h3">
Terms of service
</MDTypography>
</CardContent>
<Divider />
<MDTypography>
Welcome to Hesychia (hesychia.ai)! We're excited to have you on board. Before you start using our large language model, supplied by AI ML Operations, LLC, we need you to agree to our Terms of Service.
</MDTypography>
<MDTypography variant="h4">
1. Acceptance of Terms
</MDTypography>
<MDTypography>By logging in, you acknowledge that you have read, understood, and agree to be bound by these Terms of Service. If you don't agree, please don't use our services.</MDTypography>
<MDTypography variant="h4">2. Data Confidentiality</MDTypography>
<MDTypography>We respect your data. All information entered by you will be contained within your organization and will not be sold, shared, or disclosed to any third parties without your explicit consent.</MDTypography>
<MDTypography variant="h4">3. Use of Services</MDTypography>
<MDTypography>You agree to use our large language model for legitimate purposes only. You're responsible for ensuring that your use of our services complies with all applicable laws and regulations.</MDTypography>
<MDTypography variant="h4">4. Intellectual Property</MDTypography>
<MDTypography>All intellectual property rights in our services, including the large language model, remain the property of AI ML Operations, LLC. Any information generated by our service is for your use and AI ML Operations, LLC makes no claim to it.</MDTypography>
<MDTypography variant="h4">5. Limitation of Liability</MDTypography>
<MDTypography>In no event will we be liable for any damages, including consequential, incidental, or punitive damages, arising from your use of our services.</MDTypography>
<MDTypography variant="h4">6. Changes to Terms</MDTypography>
<MDTypography>We may update these Terms of Service from time to time. We'll notify you of significant changes, and your continued use of our services will constitute acceptance of the updated terms.</MDTypography>
<MDTypography variant="h4">7. Governing Law</MDTypography>
<MDTypography>These Terms of Service will be governed by and construed in accordance with the laws of the United States and the state of Illinois.</MDTypography>
<MDTypography>By logging in, you acknowledge that you have read and agree to these Terms of Service. If you have any questions, please don't hesitate to contact us.</MDTypography>
Happy using our services!
<Divider />
<Formik
initialValues={{
email: '',
password: '',
}}
onSubmit={handleAcknowledge}
>
{(formik) => (
<Form>
<MDButton
type={'submit'}
disabled={ formik.isSubmitting}
loading={formik.isSubmitting}
// disabled={
// !formik.isValid || !formik.dirty || formik.isSubmitting
// }
>
Acknowledge
</MDButton>
</Form>
)}
</Formik>
</Card>
</MDBox>
</PageWrapperLayout>
);
};