Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9e1449f0db | ||
|
|
0aad5394e9 | ||
|
|
6ec239d2e0 |
+11
-3
@@ -132,12 +132,20 @@ Out of band ops (not automated here):
|
|||||||
|
|
||||||
1. Google Play developer account
|
1. Google Play developer account
|
||||||
2. Privacy policy URL (product site / legal)
|
2. Privacy policy URL (product site / legal)
|
||||||
3. Data safety form (JWT auth, chat content, analytics if Tianji runs in WebView)
|
3. **Account deletion URL** (Data safety / Account deletion) → public page:
|
||||||
4. Screenshots for phone (+ tablet if targeting)
|
- Canonical: `https://hesychia.ai/account-deletion/`
|
||||||
5. Upload `.aab` to **internal testing** track before production
|
- Legacy host until DNS cutover: `https://chat.aimloperations.com/account-deletion/`
|
||||||
|
- Capacitor HashRouter: `/#/account-deletion/`
|
||||||
|
4. Data safety form (JWT auth, chat content, analytics if Tianji runs in WebView)
|
||||||
|
5. Screenshots for phone (+ tablet if targeting)
|
||||||
|
6. Upload `.aab` to **internal testing** track before production
|
||||||
|
|
||||||
|
In-app delete remains Account → **Delete my account** (`DELETE /api/user/`). The public URL
|
||||||
|
above is for Play reviewers and users who never open the app (#103).
|
||||||
|
|
||||||
## Related issues
|
## Related issues
|
||||||
|
|
||||||
- [#20](https://git.aimloperations.com/ai_ml_operations/chat_web_app/issues/20) — this Android wrap
|
- [#20](https://git.aimloperations.com/ai_ml_operations/chat_web_app/issues/20) — this Android wrap
|
||||||
- [#21](https://git.aimloperations.com/ai_ml_operations/chat_web_app/issues/21) — iOS (reuses this scaffolding)
|
- [#21](https://git.aimloperations.com/ai_ml_operations/chat_web_app/issues/21) — iOS (reuses this scaffolding)
|
||||||
- [#22](https://git.aimloperations.com/ai_ml_operations/chat_web_app/issues/22) / [#23](https://git.aimloperations.com/ai_ml_operations/chat_web_app/issues/23) / [#24](https://git.aimloperations.com/ai_ml_operations/chat_web_app/issues/24) — auth / WS / routing
|
- [#22](https://git.aimloperations.com/ai_ml_operations/chat_web_app/issues/22) / [#23](https://git.aimloperations.com/ai_ml_operations/chat_web_app/issues/23) / [#24](https://git.aimloperations.com/ai_ml_operations/chat_web_app/issues/24) — auth / WS / routing
|
||||||
|
- [#103](https://git.aimloperations.com/ai_ml_operations/chat_web_app/issues/103) — Play public account-deletion URL
|
||||||
|
|||||||
@@ -107,9 +107,15 @@ Without `keystore.properties`, release builds are unsigned; debug builds do not
|
|||||||
|
|
||||||
1. [Play Console](https://play.google.com/console) → app **Hesychia** (`ai.hesychia.chat`).
|
1. [Play Console](https://play.google.com/console) → app **Hesychia** (`ai.hesychia.chat`).
|
||||||
2. Complete store listing: privacy policy URL, data-safety form, screenshots.
|
2. Complete store listing: privacy policy URL, data-safety form, screenshots.
|
||||||
3. **Testing → Internal testing** → create release → upload `app-release.aab`.
|
3. Set **Account deletion URL** to the public instructions page (no sign-in required):
|
||||||
4. Add testers, roll out internal track, verify on devices.
|
- `https://hesychia.ai/account-deletion/` (preferred product host)
|
||||||
5. Promote to closed / open / production only after internal QA passes.
|
- or `https://chat.aimloperations.com/account-deletion/` until hesychia.ai cutover
|
||||||
|
4. **Testing → Internal testing** → create release → upload `app-release.aab`.
|
||||||
|
5. Add testers, roll out internal track, verify on devices.
|
||||||
|
6. Promote to closed / open / production only after internal QA passes.
|
||||||
|
|
||||||
|
See also [`../ANDROID.md`](../ANDROID.md) Play checklist and issue
|
||||||
|
[#103](https://git.aimloperations.com/ai_ml_operations/chat_web_app/issues/103).
|
||||||
|
|
||||||
Each new upload needs a higher `versionCode`.
|
Each new upload needs a higher `versionCode`.
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import NotFound from './llm-fe/pages/NotFound/NotFound';
|
|||||||
import { AuthContext } from './llm-fe/contexts/AuthContext';
|
import { AuthContext } from './llm-fe/contexts/AuthContext';
|
||||||
import TermsOfService from './llm-fe/pages/TermsOfService/TermsOfService';
|
import TermsOfService from './llm-fe/pages/TermsOfService/TermsOfService';
|
||||||
import PublicTermsOfServicePage from './llm-fe/pages/TermsOfService/PublicTermsOfServicePage';
|
import PublicTermsOfServicePage from './llm-fe/pages/TermsOfService/PublicTermsOfServicePage';
|
||||||
|
import AccountDeletionPage from './llm-fe/pages/AccountDeletion/AccountDeletionPage';
|
||||||
import SetPassword from './llm-fe/components/SetPassword/SetPassword';
|
import SetPassword from './llm-fe/components/SetPassword/SetPassword';
|
||||||
import AsyncDashboard2 from './llm-fe/pages/AsyncDashboard2/AsyncDashboard2';
|
import AsyncDashboard2 from './llm-fe/pages/AsyncDashboard2/AsyncDashboard2';
|
||||||
import FeedbackPage2 from './llm-fe/pages/FeedbackPage2/FeedbackPage2';
|
import FeedbackPage2 from './llm-fe/pages/FeedbackPage2/FeedbackPage2';
|
||||||
@@ -71,6 +72,8 @@ class App extends Component {
|
|||||||
{/* Public legal pages (also used for post-login ToS acknowledge) */}
|
{/* Public legal pages (also used for post-login ToS acknowledge) */}
|
||||||
<Route path={"/terms_of_service/"} Component={TermsOfService} />
|
<Route path={"/terms_of_service/"} Component={TermsOfService} />
|
||||||
<Route path={"/legal/terms-of-service"} Component={PublicTermsOfServicePage} />
|
<Route path={"/legal/terms-of-service"} Component={PublicTermsOfServicePage} />
|
||||||
|
{/* Play Console account-deletion URL (#103) — must stay public */}
|
||||||
|
<Route path={"/account-deletion/"} Component={AccountDeletionPage} />
|
||||||
|
|
||||||
<Route element={<ProtectedRoutes />}>
|
<Route element={<ProtectedRoutes />}>
|
||||||
<Route path={"/"} index={true} Component={AsyncDashboard2} />
|
<Route path={"/"} index={true} Component={AsyncDashboard2} />
|
||||||
|
|||||||
+10
-16
@@ -36,7 +36,7 @@ describe('ConversationDetailCard citations (#98)', () => {
|
|||||||
expect(out).toContain('```\n[9]\n```');
|
expect(out).toContain('```\n[9]\n```');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders Sources list from citations prop', () => {
|
it('does not render Sources list card', () => {
|
||||||
renderCard({
|
renderCard({
|
||||||
message: 'Answer with [1]',
|
message: 'Answer with [1]',
|
||||||
user_created: false,
|
user_created: false,
|
||||||
@@ -49,24 +49,13 @@ describe('ConversationDetailCard citations (#98)', () => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
expect(screen.getByLabelText('Sources')).toBeInTheDocument();
|
|
||||||
const link = screen.getByRole('link', { name: 'Example Source' });
|
|
||||||
expect(link).toHaveAttribute('href', 'https://example.com/a');
|
|
||||||
expect(link).toHaveAttribute('rel', 'noopener noreferrer');
|
|
||||||
expect(link).toHaveAttribute('target', '_blank');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('renders nothing for Sources when citations empty', () => {
|
|
||||||
renderCard({
|
|
||||||
message: 'No sources here',
|
|
||||||
user_created: false,
|
|
||||||
citations: [],
|
|
||||||
});
|
|
||||||
expect(screen.queryByLabelText('Sources')).not.toBeInTheDocument();
|
expect(screen.queryByLabelText('Sources')).not.toBeInTheDocument();
|
||||||
|
expect(screen.getByRole('button', { name: 'Source 1' })).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('highlights source when inline citation clicked', async () => {
|
it('opens citation URL in a new tab when inline marker clicked', async () => {
|
||||||
const user = userEvent.setup();
|
const user = userEvent.setup();
|
||||||
|
const openSpy = jest.spyOn(window, 'open').mockImplementation(() => null);
|
||||||
renderCard({
|
renderCard({
|
||||||
message: 'See [1]',
|
message: 'See [1]',
|
||||||
user_created: false,
|
user_created: false,
|
||||||
@@ -75,6 +64,11 @@ describe('ConversationDetailCard citations (#98)', () => {
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
await user.click(screen.getByRole('button', { name: 'Source 1' }));
|
await user.click(screen.getByRole('button', { name: 'Source 1' }));
|
||||||
expect(document.getElementById('citation-source-1')).toBeInTheDocument();
|
expect(openSpy).toHaveBeenCalledWith(
|
||||||
|
'https://example.com',
|
||||||
|
'_blank',
|
||||||
|
'noopener,noreferrer',
|
||||||
|
);
|
||||||
|
openSpy.mockRestore();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useCallback, useMemo, useState } from 'react';
|
import React, { useCallback, useMemo } from 'react';
|
||||||
import Markdown from 'markdown-to-jsx';
|
import Markdown from 'markdown-to-jsx';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import styled, { keyframes } from 'styled-components';
|
import styled, { keyframes } from 'styled-components';
|
||||||
@@ -6,7 +6,6 @@ import { isRagFeatureNotAllowed, parseChatErrorPayload } from '../../utils/chatE
|
|||||||
import type { ActivityHistoryEntry, Citation } from '../../utils/wsFrames';
|
import type { ActivityHistoryEntry, Citation } from '../../utils/wsFrames';
|
||||||
import type { PromptRating } from '../../utils/promptFeedback';
|
import type { PromptRating } from '../../utils/promptFeedback';
|
||||||
import CustomPreBlock from '../CustomPreBlock/CustomPreBlock';
|
import CustomPreBlock from '../CustomPreBlock/CustomPreBlock';
|
||||||
import SourcesList from '../SourcesList/SourcesList';
|
|
||||||
import MessageActions from '../MessageActions/MessageActions';
|
import MessageActions from '../MessageActions/MessageActions';
|
||||||
import ActivityIndicator from '../ActivityIndicator/ActivityIndicator';
|
import ActivityIndicator from '../ActivityIndicator/ActivityIndicator';
|
||||||
|
|
||||||
@@ -209,8 +208,6 @@ const ConversationDetailCard = ({
|
|||||||
activityHistory = [],
|
activityHistory = [],
|
||||||
activityInterrupted = false,
|
activityInterrupted = false,
|
||||||
}: ConversationDetailCardProps): JSX.Element => {
|
}: ConversationDetailCardProps): JSX.Element => {
|
||||||
const [highlightIndex, setHighlightIndex] = useState<number | null>(null);
|
|
||||||
|
|
||||||
const CitationMark = useCallback(
|
const CitationMark = useCallback(
|
||||||
({ indices }: { indices?: string }) => {
|
({ indices }: { indices?: string }) => {
|
||||||
const list = (indices || '')
|
const list = (indices || '')
|
||||||
@@ -229,12 +226,9 @@ const ConversationDetailCard = ({
|
|||||||
aria-label={`Source ${index}`}
|
aria-label={`Source ${index}`}
|
||||||
title={citation?.title || `Source ${index}`}
|
title={citation?.title || `Source ${index}`}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setHighlightIndex(index);
|
|
||||||
if (citation?.url) {
|
if (citation?.url) {
|
||||||
// Prefer scroll/highlight; URL still available from Sources list
|
window.open(citation.url, '_blank', 'noopener,noreferrer');
|
||||||
}
|
}
|
||||||
const el = document.getElementById(`citation-source-${index}`);
|
|
||||||
el?.scrollIntoView?.({ behavior: 'smooth', block: 'nearest' });
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
[{index}]
|
[{index}]
|
||||||
@@ -346,10 +340,6 @@ const ConversationDetailCard = ({
|
|||||||
</Markdown>
|
</Markdown>
|
||||||
</Bubble>
|
</Bubble>
|
||||||
|
|
||||||
{!user_created && citations.length > 0 && (
|
|
||||||
<SourcesList citations={citations} highlightIndex={highlightIndex} />
|
|
||||||
)}
|
|
||||||
|
|
||||||
<MessageActions
|
<MessageActions
|
||||||
promptId={promptId}
|
promptId={promptId}
|
||||||
rawMarkdown={rawForCopy}
|
rawMarkdown={rawForCopy}
|
||||||
|
|||||||
@@ -1,138 +0,0 @@
|
|||||||
import React, { useEffect, useRef, useState } from 'react';
|
|
||||||
import styled from 'styled-components';
|
|
||||||
import type { Citation } from '../../utils/wsFrames';
|
|
||||||
|
|
||||||
const SourcesRoot = styled.aside`
|
|
||||||
margin-top: 0.65rem;
|
|
||||||
max-width: 80%;
|
|
||||||
min-width: 0;
|
|
||||||
width: 100%;
|
|
||||||
padding: 0.65rem 0.85rem;
|
|
||||||
border-radius: 0.75rem;
|
|
||||||
border: 1px solid ${({ theme }) => theme.colors.cardBorder};
|
|
||||||
background: ${({ theme }) =>
|
|
||||||
theme.darkMode ? 'rgba(255, 255, 255, 0.04)' : 'rgba(0, 0, 0, 0.03)'};
|
|
||||||
color: ${({ theme }) => theme.colors.text};
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
max-width: 92%;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const SourcesTitle = styled.h4`
|
|
||||||
margin: 0 0 0.5rem;
|
|
||||||
font-size: 0.8rem;
|
|
||||||
font-weight: 700;
|
|
||||||
letter-spacing: 0.02em;
|
|
||||||
text-transform: uppercase;
|
|
||||||
opacity: 0.75;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const SourceList = styled.ol`
|
|
||||||
margin: 0;
|
|
||||||
padding-left: 1.25rem;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.4rem;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const SourceItem = styled.li<{ $highlight: boolean }>`
|
|
||||||
font-size: 0.85rem;
|
|
||||||
line-height: 1.4;
|
|
||||||
border-radius: 0.35rem;
|
|
||||||
padding: 0.15rem 0.25rem;
|
|
||||||
outline: ${({ $highlight, theme }) =>
|
|
||||||
$highlight ? `2px solid ${theme.main}` : 'none'};
|
|
||||||
background: ${({ $highlight, theme }) =>
|
|
||||||
$highlight
|
|
||||||
? theme.darkMode
|
|
||||||
? 'rgba(255,255,255,0.08)'
|
|
||||||
: 'rgba(0,0,0,0.06)'
|
|
||||||
: 'transparent'};
|
|
||||||
transition: background 0.2s ease, outline 0.2s ease;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const SourceLink = styled.a`
|
|
||||||
color: ${({ theme }) => (theme.darkMode ? '#a0c4ff' : theme.main)};
|
|
||||||
text-decoration: underline;
|
|
||||||
word-break: break-word;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const SourceMeta = styled.span`
|
|
||||||
display: block;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
opacity: 0.65;
|
|
||||||
margin-top: 0.1rem;
|
|
||||||
`;
|
|
||||||
|
|
||||||
function domainFromUrl(url: string): string {
|
|
||||||
try {
|
|
||||||
return new URL(url).hostname.replace(/^www\./, '');
|
|
||||||
} catch {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type SourcesListProps = {
|
|
||||||
citations: Citation[];
|
|
||||||
highlightIndex?: number | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const SourcesList = ({
|
|
||||||
citations,
|
|
||||||
highlightIndex = null,
|
|
||||||
}: SourcesListProps): JSX.Element | null => {
|
|
||||||
const itemRefs = useRef<Record<number, HTMLLIElement | null>>({});
|
|
||||||
const [active, setActive] = useState<number | null>(highlightIndex);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setActive(highlightIndex ?? null);
|
|
||||||
if (highlightIndex == null) return;
|
|
||||||
const el = itemRefs.current[highlightIndex];
|
|
||||||
el?.scrollIntoView?.({ behavior: 'smooth', block: 'nearest' });
|
|
||||||
}, [highlightIndex]);
|
|
||||||
|
|
||||||
if (!citations.length) return null;
|
|
||||||
|
|
||||||
const sorted = [...citations].sort((a, b) => a.index - b.index);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SourcesRoot aria-label="Sources">
|
|
||||||
<SourcesTitle>Sources</SourcesTitle>
|
|
||||||
<SourceList>
|
|
||||||
{sorted.map((citation) => {
|
|
||||||
const domain = domainFromUrl(citation.url);
|
|
||||||
return (
|
|
||||||
<SourceItem
|
|
||||||
key={citation.index}
|
|
||||||
id={`citation-source-${citation.index}`}
|
|
||||||
$highlight={active === citation.index}
|
|
||||||
ref={(node) => {
|
|
||||||
itemRefs.current[citation.index] = node;
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{citation.url ? (
|
|
||||||
<SourceLink
|
|
||||||
href={citation.url}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
{citation.title || `Source ${citation.index}`}
|
|
||||||
</SourceLink>
|
|
||||||
) : (
|
|
||||||
<span>{citation.title || `Source ${citation.index}`}</span>
|
|
||||||
)}
|
|
||||||
{(citation.published_at || domain) && (
|
|
||||||
<SourceMeta>
|
|
||||||
{[citation.published_at, domain].filter(Boolean).join(' · ')}
|
|
||||||
</SourceMeta>
|
|
||||||
)}
|
|
||||||
</SourceItem>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</SourceList>
|
|
||||||
</SourcesRoot>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default SourcesList;
|
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
|
import { render, screen } from '@testing-library/react';
|
||||||
|
import AccountDeletionPage from './AccountDeletionPage';
|
||||||
|
|
||||||
|
describe('AccountDeletionPage', () => {
|
||||||
|
it('renders Hesychia deletion steps and retention copy without auth', () => {
|
||||||
|
render(
|
||||||
|
<MemoryRouter>
|
||||||
|
<AccountDeletionPage />
|
||||||
|
</MemoryRouter>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
screen.getByRole('heading', { name: /Hesychia — Request account deletion/i }),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
expect(screen.getByText(/ai\.hesychia\.chat/i)).toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
screen.getByRole('heading', { name: /How to delete your account/i }),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
expect(screen.getByText(/Delete my account/i)).toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
screen.getByRole('heading', { name: /Deactivated immediately/i }),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
screen.getByRole('heading', { name: /Data retained after soft-delete/i }),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
expect(screen.getByRole('heading', { name: /Retention period/i })).toBeInTheDocument();
|
||||||
|
expect(screen.getByText(/no automatic hard-purge schedule/i)).toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
screen.getByRole('heading', { name: /Cannot access your account/i }),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
expect(screen.getByText(/30 days/i)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Box, Button, Card, CardContent, Divider, Stack, Typography } from '@mui/material';
|
||||||
|
import { Link as RouterLink } from 'react-router-dom';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Public Play / App Store account-deletion instructions (#103).
|
||||||
|
* No auth required — reviewers and signed-out users must be able to read this page.
|
||||||
|
*/
|
||||||
|
const AccountDeletionPage = (): JSX.Element => (
|
||||||
|
<Box sx={{ py: 4, px: 2, minHeight: '100vh', bgcolor: 'background.default' }}>
|
||||||
|
<Card sx={{ maxWidth: 900, mx: 'auto', width: '100%' }}>
|
||||||
|
<CardContent>
|
||||||
|
<Typography variant="h3" component="h1" gutterBottom>
|
||||||
|
Hesychia — Request account deletion
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="subtitle1" color="text.secondary" gutterBottom>
|
||||||
|
Hesychia (ai.hesychia.chat) · hesychia.ai · AI ML Operations, LLC
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body1" color="text.secondary">
|
||||||
|
Use the steps below to request deletion of your Hesychia account. This page does not
|
||||||
|
require sign-in to read.
|
||||||
|
</Typography>
|
||||||
|
</CardContent>
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
<CardContent>
|
||||||
|
<Stack spacing={3}>
|
||||||
|
<Box>
|
||||||
|
<Typography variant="h5" component="h2" gutterBottom>
|
||||||
|
How to delete your account
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body1" component="ol" sx={{ pl: 2.5, m: 0 }}>
|
||||||
|
<li>
|
||||||
|
Open <strong>Hesychia</strong> in the mobile app or at{' '}
|
||||||
|
<a href="https://hesychia.ai" rel="noopener noreferrer">
|
||||||
|
hesychia.ai
|
||||||
|
</a>
|
||||||
|
.
|
||||||
|
</li>
|
||||||
|
<li>Sign in with the account you want deleted.</li>
|
||||||
|
<li>
|
||||||
|
Go to <strong>Account</strong>.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Choose <strong>Delete my account</strong>, then confirm by typing your account
|
||||||
|
email.
|
||||||
|
</li>
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
<Typography variant="h5" component="h2" gutterBottom>
|
||||||
|
What happens when you delete
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Typography variant="h6" component="h3" gutterBottom sx={{ mt: 2 }}>
|
||||||
|
Deactivated immediately
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body1" component="ul" sx={{ pl: 2.5, m: 0 }}>
|
||||||
|
<li>Your account is marked deleted and set inactive.</li>
|
||||||
|
<li>You lose access to Hesychia and cannot sign in again with that account.</li>
|
||||||
|
<li>Your conversations are hidden from the product.</li>
|
||||||
|
<li>Active sessions are invalidated (refresh tokens are blacklisted).</li>
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Typography variant="h6" component="h3" gutterBottom sx={{ mt: 2 }}>
|
||||||
|
Data retained after soft-delete
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body1" paragraph>
|
||||||
|
Hesychia currently uses a <strong>soft-delete</strong> process. After you delete your
|
||||||
|
account, personal data associated with the account (including profile details,
|
||||||
|
conversation content, uploaded documents / RAG vectors, and authentication event
|
||||||
|
records) may remain in our systems for administration, security, and audit purposes.
|
||||||
|
This is not a full erasure of all stored data.
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Typography variant="h6" component="h3" gutterBottom>
|
||||||
|
Retention period
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body1" paragraph sx={{ mb: 0 }}>
|
||||||
|
There is currently <strong>no automatic hard-purge schedule</strong>. Soft-deleted
|
||||||
|
data is retained until a future purge process is implemented or until we process a
|
||||||
|
separate retention / erasure request through the contact path below. We will not claim
|
||||||
|
complete erasure while soft-delete-only remains in effect.
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
<Typography variant="h5" component="h2" gutterBottom>
|
||||||
|
Cannot access your account?
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body1" paragraph>
|
||||||
|
If you cannot sign in to complete in-app deletion, contact AI ML Operations, LLC via{' '}
|
||||||
|
<a href="https://www.aimloperations.com" rel="noopener noreferrer">
|
||||||
|
aimloperations.com
|
||||||
|
</a>{' '}
|
||||||
|
and include the email address used for your Hesychia account. We aim to respond to
|
||||||
|
account-deletion requests within <strong>30 days</strong>.
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
Staff accounts cannot self-delete in the app; use the contact path above.
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Stack>
|
||||||
|
</CardContent>
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
<CardContent>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
Already have an account?{' '}
|
||||||
|
<Button component={RouterLink} to="/signin/" size="small">
|
||||||
|
Sign in
|
||||||
|
</Button>{' '}
|
||||||
|
·{' '}
|
||||||
|
<Button component={RouterLink} to="/account/" size="small">
|
||||||
|
Account
|
||||||
|
</Button>{' '}
|
||||||
|
·{' '}
|
||||||
|
<Button component={RouterLink} to="/terms_of_service/" size="small">
|
||||||
|
Terms of Service
|
||||||
|
</Button>
|
||||||
|
</Typography>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default AccountDeletionPage;
|
||||||
@@ -252,32 +252,6 @@ const IconButton = styled.button`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledSelect = styled.select`
|
|
||||||
background: transparent;
|
|
||||||
border: none;
|
|
||||||
flex-shrink: 0;
|
|
||||||
max-width: 7rem;
|
|
||||||
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};
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
max-width: 5.5rem;
|
|
||||||
font-size: 0.8rem;
|
|
||||||
padding: 0.4rem 0.2rem;
|
|
||||||
margin-right: 0.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
||||||
@@ -343,11 +317,13 @@ const validationSchema = Yup.object().shape({
|
|||||||
.required("This is required"),
|
.required("This is required"),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/** Fixed until product re-exposes a model picker (#106). */
|
||||||
|
const DEFAULT_MODEL_NAME = "THINKING";
|
||||||
|
|
||||||
type PromptValues = {
|
type PromptValues = {
|
||||||
prompt: string;
|
prompt: string;
|
||||||
file: Blob | null;
|
file: Blob | null;
|
||||||
fileType: string | null;
|
fileType: string | null;
|
||||||
modelName: string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const AlwaysScrollToBottom = ({ trigger }: { trigger: string | number }): JSX.Element => {
|
const AlwaysScrollToBottom = ({ trigger }: { trigger: string | number }): JSX.Element => {
|
||||||
@@ -447,7 +423,7 @@ const AsyncDashboardInner = (): JSX.Element => {
|
|||||||
})();
|
})();
|
||||||
|
|
||||||
const handlePromptSubmit = async (
|
const handlePromptSubmit = async (
|
||||||
{ prompt, file, fileType, modelName }: PromptValues,
|
{ prompt, file, fileType }: PromptValues,
|
||||||
{ resetForm }: any,
|
{ resetForm }: any,
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
const trimmedPrompt = (prompt ?? "").trim();
|
const trimmedPrompt = (prompt ?? "").trim();
|
||||||
@@ -464,7 +440,7 @@ const AsyncDashboardInner = (): JSX.Element => {
|
|||||||
|
|
||||||
conversationRef.current = tempConversations;
|
conversationRef.current = tempConversations;
|
||||||
setConversationDetails(tempConversations);
|
setConversationDetails(tempConversations);
|
||||||
sendMessage(trimmedPrompt, selectedConversation, file, fileType, modelName);
|
sendMessage(trimmedPrompt, selectedConversation, file, fileType, DEFAULT_MODEL_NAME);
|
||||||
trackEvent(AnalyticsEvents.MESSAGE_SENT, {
|
trackEvent(AnalyticsEvents.MESSAGE_SENT, {
|
||||||
hasConversation: Boolean(selectedConversation),
|
hasConversation: Boolean(selectedConversation),
|
||||||
hasAttachment: Boolean(file),
|
hasAttachment: Boolean(file),
|
||||||
@@ -474,7 +450,6 @@ const AsyncDashboardInner = (): JSX.Element => {
|
|||||||
prompt: "",
|
prompt: "",
|
||||||
file: null,
|
file: null,
|
||||||
fileType: null,
|
fileType: null,
|
||||||
modelName: modelName, // Keep the selected model
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -640,7 +615,6 @@ const AsyncDashboardInner = (): JSX.Element => {
|
|||||||
prompt: "",
|
prompt: "",
|
||||||
file: null,
|
file: null,
|
||||||
fileType: null,
|
fileType: null,
|
||||||
modelName: "FAST",
|
|
||||||
}}
|
}}
|
||||||
validationSchema={validationSchema}
|
validationSchema={validationSchema}
|
||||||
onSubmit={handlePromptSubmit}
|
onSubmit={handlePromptSubmit}
|
||||||
@@ -691,14 +665,6 @@ const AsyncDashboardInner = (): JSX.Element => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Field
|
|
||||||
as={StyledSelect}
|
|
||||||
name="modelName"
|
|
||||||
>
|
|
||||||
<option value="FAST">FAST</option>
|
|
||||||
<option value="THINKING">THINKING</option>
|
|
||||||
</Field>
|
|
||||||
|
|
||||||
<Field name="prompt">
|
<Field name="prompt">
|
||||||
{({ field }: any) => (
|
{({ field }: any) => (
|
||||||
<StyledInput
|
<StyledInput
|
||||||
|
|||||||
Reference in New Issue
Block a user