Translate correspondence flow and localize AI system details
This commit is contained in:
@@ -31,6 +31,7 @@ import { api, getApiErrorMessage } from "../api";
|
||||
import { useToast } from "../toast";
|
||||
import { CorrespondenceMessage, GmailMessageSummary, GmailStatus } from "../types";
|
||||
import { useDialogActions } from "../dialogs";
|
||||
import { useI18n } from "../i18n/I18nProvider";
|
||||
|
||||
function parseRawEmail(raw: string): { subject?: string; date?: string; from?: string; to?: string; body: string } {
|
||||
const lines = raw.replace(/\r\n/g, "\n").split("\n");
|
||||
@@ -80,6 +81,7 @@ function scoreMessage(message: GmailMessageSummary, query: string, messages: Cor
|
||||
export default function Correspondence({ jobId }: { jobId: number }) {
|
||||
const theme = useTheme();
|
||||
const { toast } = useToast();
|
||||
const { t } = useI18n();
|
||||
const { confirmAction } = useDialogActions();
|
||||
const [messages, setMessages] = useState<CorrespondenceMessage[]>([]);
|
||||
const [from, setFrom] = useState<"Me" | "Company">("Me");
|
||||
@@ -157,18 +159,18 @@ export default function Correspondence({ jobId }: { jobId: number }) {
|
||||
const data = event.data as { source?: string; status?: string; message?: string };
|
||||
if (data?.source !== "jobtracker-gmail-oauth") return;
|
||||
if (data.status === "connected") {
|
||||
toast(data.message || "Gmail connected.", "success");
|
||||
toast(data.message || t("googleLinkedSuccess"), "success");
|
||||
void loadGmailStatus();
|
||||
setImportTab(1);
|
||||
void loadGmailMessages();
|
||||
} else {
|
||||
toast(data.message || "Gmail connection failed.", "error");
|
||||
toast(data.message || t("googleAuthFailed"), "error");
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("message", onMessage);
|
||||
return () => window.removeEventListener("message", onMessage);
|
||||
}, [loadGmailMessages, loadGmailStatus, toast]);
|
||||
}, [loadGmailMessages, loadGmailStatus, t, toast]);
|
||||
|
||||
const canSend = useMemo(() => text.trim().length > 0, [text]);
|
||||
|
||||
@@ -213,7 +215,7 @@ export default function Correspondence({ jobId }: { jobId: number }) {
|
||||
const importEmail = async () => {
|
||||
const parsed = parseRawEmail(rawEmail);
|
||||
if (!parsed.body && !parsed.subject && !rawEmail.trim()) {
|
||||
toast("Paste an email first.", "error");
|
||||
toast(t("addJobModalPasteUrlFirst"), "error");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -229,7 +231,7 @@ export default function Correspondence({ jobId }: { jobId: number }) {
|
||||
setImportOpen(false);
|
||||
setRawEmail("");
|
||||
await load();
|
||||
toast("Email logged.", "success");
|
||||
toast(t("correspondenceLogEmail"), "success");
|
||||
} catch (error) {
|
||||
toast(getApiErrorMessage(error, "Failed to import email."), "error");
|
||||
}
|
||||
@@ -250,14 +252,14 @@ export default function Correspondence({ jobId }: { jobId: number }) {
|
||||
await api.delete("/gmail/connection");
|
||||
setGmailStatus({ connected: false });
|
||||
setGmailMessages([]);
|
||||
toast("Gmail disconnected.", "success");
|
||||
toast(t("googleUnlinked"), "success");
|
||||
} catch (error) {
|
||||
toast(getApiErrorMessage(error, "Failed to disconnect Gmail."), "error");
|
||||
}
|
||||
};
|
||||
|
||||
const deleteMessage = async (messageId: number) => {
|
||||
if (!(await confirmAction("Remove this correspondence message?", { title: "Delete message", confirmLabel: "Delete", destructive: true }))) return;
|
||||
if (!(await confirmAction("Remove this correspondence message?", { title: "Delete message", confirmLabel: t("jobTableDeleteSelected"), destructive: true }))) return;
|
||||
try {
|
||||
await api.delete(`/correspondence/${messageId}`);
|
||||
await load();
|
||||
@@ -272,7 +274,7 @@ export default function Correspondence({ jobId }: { jobId: number }) {
|
||||
setImportingMessageId(messageId);
|
||||
await api.post("/gmail/import", { jobApplicationId: jobId, messageId });
|
||||
await load();
|
||||
toast("Email imported from Gmail.", "success");
|
||||
toast(t("correspondenceImportEmail"), "success");
|
||||
} catch (error: any) {
|
||||
toast(getApiErrorMessage(error, "Failed to import Gmail message."), "error");
|
||||
} finally {
|
||||
@@ -310,7 +312,7 @@ export default function Correspondence({ jobId }: { jobId: number }) {
|
||||
<Typography sx={{ whiteSpace: "pre-wrap", lineHeight: 1.35 }}>{m.content}</Typography>
|
||||
<Box sx={{ display: "flex", justifyContent: "space-between", gap: 1, alignItems: "flex-end", mt: 0.75 }}>
|
||||
<Typography variant="caption" sx={{ color: "text.secondary" }}>
|
||||
{isMe ? "Me" : "Company"}{m.channel ? ` - ${m.channel}` : ""}{m.date ? ` - ${new Date(m.date).toLocaleString()}` : ""}
|
||||
{isMe ? t("correspondenceMe") : t("correspondenceCompany")}{m.channel ? ` - ${m.channel}` : ""}{m.date ? ` - ${new Date(m.date).toLocaleString()}` : ""}
|
||||
</Typography>
|
||||
<IconButton size="small" onClick={() => void deleteMessage(m.id)} sx={{ color: "text.secondary" }}>
|
||||
<DeleteOutlineIcon fontSize="small" />
|
||||
@@ -326,47 +328,47 @@ export default function Correspondence({ jobId }: { jobId: number }) {
|
||||
|
||||
<Box sx={{ display: "flex", gap: 1, alignItems: "flex-start", mt: 1.5, flexWrap: "wrap" }}>
|
||||
<ToggleButtonGroup exclusive value={from} onChange={(_, v) => v && setFrom(v)} size="small">
|
||||
<ToggleButton value="Me">Me</ToggleButton>
|
||||
<ToggleButton value="Company">Company</ToggleButton>
|
||||
<ToggleButton value="Me">{t("correspondenceMe")}</ToggleButton>
|
||||
<ToggleButton value="Company">{t("correspondenceCompany")}</ToggleButton>
|
||||
</ToggleButtonGroup>
|
||||
|
||||
<Button variant="outlined" size="small" onClick={() => setImportOpen(true)}>Import Email</Button>
|
||||
<Button variant="outlined" size="small" onClick={() => setImportOpen(true)}>{t("correspondenceImportEmail")}</Button>
|
||||
|
||||
<TextField label="Log note or message" value={text} onChange={(e) => setText(e.target.value)} multiline minRows={3} sx={{ flex: "1 1 320px" }} helperText={`${text.length} characters`} />
|
||||
<TextField label={t("correspondenceLogNoteOrMessage")} value={text} onChange={(e) => setText(e.target.value)} multiline minRows={3} sx={{ flex: "1 1 320px" }} helperText={t("correspondenceCharacters", { count: text.length })} />
|
||||
|
||||
<Button variant="contained" onClick={send} disabled={!canSend}>Add</Button>
|
||||
<Button variant="contained" onClick={send} disabled={!canSend}>{t("correspondenceAdd")}</Button>
|
||||
</Box>
|
||||
|
||||
<Dialog open={importOpen} onClose={() => setImportOpen(false)} fullWidth maxWidth="md">
|
||||
<DialogTitle>Import Email</DialogTitle>
|
||||
<DialogTitle>{t("correspondenceImportTitle")}</DialogTitle>
|
||||
<DialogContent>
|
||||
<Tabs value={importTab} onChange={(_, v) => setImportTab(v)} sx={{ mb: 2 }}>
|
||||
<Tab label="Paste email" />
|
||||
<Tab label="Gmail" />
|
||||
<Tab label={t("correspondencePasteEmail")} />
|
||||
<Tab label={t("google")} />
|
||||
</Tabs>
|
||||
|
||||
{importTab === 0 ? (
|
||||
<>
|
||||
<Typography sx={{ color: "text.secondary", mb: 1 }}>Paste raw email text (headers optional). We parse Subject and Date when present.</Typography>
|
||||
<Typography sx={{ color: "text.secondary", mb: 1 }}>{t("correspondencePasteEmailHelp")}</Typography>
|
||||
<TextField multiline minRows={10} fullWidth value={rawEmail} onChange={(e) => setRawEmail(e.target.value)} placeholder={"Subject: ...\nDate: ...\nFrom: ...\nTo: ...\n\nBody..."} />
|
||||
</>
|
||||
) : (
|
||||
<Box sx={{ display: "flex", flexDirection: "column", gap: 2 }}>
|
||||
<Box sx={{ display: "flex", justifyContent: "space-between", gap: 1, flexWrap: "wrap", alignItems: "center" }}>
|
||||
<Box>
|
||||
<Typography sx={{ fontWeight: 800 }}>Google Gmail</Typography>
|
||||
<Typography sx={{ fontWeight: 800 }}>{t("correspondenceGoogleGmail")}</Typography>
|
||||
<Typography variant="body2" sx={{ color: "text.secondary" }}>
|
||||
{gmailLoading ? "Checking connection..." : gmailStatus?.connected ? `Connected as ${gmailStatus.gmailAddress}` : "Connect your Gmail account to browse recent emails."}
|
||||
{gmailLoading ? t("correspondenceCheckingConnection") : gmailStatus?.connected ? t("correspondenceConnectedAs", { email: gmailStatus.gmailAddress || "" }) : t("correspondenceConnectGmailHint")}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box sx={{ display: "flex", gap: 1, flexWrap: "wrap" }}>
|
||||
{gmailStatus?.connected ? (
|
||||
<>
|
||||
<Button variant="outlined" onClick={() => void loadGmailMessages()} disabled={gmailMessagesLoading}>Refresh</Button>
|
||||
<Button variant="outlined" color="error" onClick={() => void disconnectGmail()}>Disconnect</Button>
|
||||
<Button variant="outlined" onClick={() => void loadGmailMessages()} disabled={gmailMessagesLoading}>{t("correspondenceRefresh")}</Button>
|
||||
<Button variant="outlined" color="error" onClick={() => void disconnectGmail()}>{t("correspondenceDisconnect")}</Button>
|
||||
</>
|
||||
) : (
|
||||
<Button variant="contained" onClick={() => void connectGmail()}>Connect Gmail</Button>
|
||||
<Button variant="contained" onClick={() => void connectGmail()}>{t("correspondenceConnectGmail")}</Button>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
@@ -379,8 +381,8 @@ export default function Correspondence({ jobId }: { jobId: number }) {
|
||||
))}
|
||||
</Box>
|
||||
<Box sx={{ display: "flex", gap: 1, flexWrap: "wrap", alignItems: "center" }}>
|
||||
<TextField label="Search Gmail" value={gmailQuery} onChange={(e) => setGmailQuery(e.target.value)} placeholder="from:company@example.com OR interview" size="small" fullWidth />
|
||||
<Button variant="outlined" onClick={() => void loadGmailMessages()} disabled={gmailMessagesLoading}>Search</Button>
|
||||
<TextField label={t("correspondenceSearchGmail")} value={gmailQuery} onChange={(e) => setGmailQuery(e.target.value)} placeholder={t("correspondenceSearchGmailPlaceholder")} size="small" fullWidth />
|
||||
<Button variant="outlined" onClick={() => void loadGmailMessages()} disabled={gmailMessagesLoading}>{t("correspondenceSearch")}</Button>
|
||||
</Box>
|
||||
{gmailStatus.lastSyncedAt ? <Chip label={`Last synced ${new Date(gmailStatus.lastSyncedAt).toLocaleString()}`} size="small" /> : null}
|
||||
<Paper variant="outlined" sx={{ maxHeight: 420, overflowY: "auto" }}>
|
||||
@@ -409,7 +411,7 @@ export default function Correspondence({ jobId }: { jobId: number }) {
|
||||
<ListItemButton sx={{ alignItems: "flex-start", px: 0, py: 1 }}>
|
||||
<ListItemText
|
||||
primary={<Box sx={{ display: "flex", justifyContent: "space-between", gap: 2, alignItems: "center", flexWrap: "wrap" }}><Typography sx={{ fontWeight: 700 }}>{message.subject || "(No subject)"}</Typography><Typography variant="caption" sx={{ color: "text.secondary" }}>{message.date ? new Date(message.date).toLocaleString() : ""}</Typography></Box>}
|
||||
secondary={<Box sx={{ mt: 0.5 }}><Typography variant="body2" sx={{ color: "text.primary" }}>From: {message.from || "Unknown"}</Typography><Typography variant="body2" sx={{ color: "text.secondary", mt: 0.25 }}>{message.snippet}</Typography></Box>}
|
||||
secondary={<Box sx={{ mt: 0.5 }}><Typography variant="body2" sx={{ color: "text.primary" }}>From: {message.from || t("correspondenceUnknown")}</Typography><Typography variant="body2" sx={{ color: "text.secondary", mt: 0.25 }}>{message.snippet}</Typography></Box>}
|
||||
/>
|
||||
<Button variant="contained" size="small" disabled={importingMessageId === message.id} onClick={() => void importGmailMessage(message.id)}>
|
||||
{importingMessageId === message.id ? "Importing..." : "Import"}
|
||||
@@ -429,8 +431,8 @@ export default function Correspondence({ jobId }: { jobId: number }) {
|
||||
)}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => setImportOpen(false)}>Close</Button>
|
||||
{importTab === 0 ? <Button variant="contained" onClick={importEmail}>Log Email</Button> : null}
|
||||
<Button onClick={() => setImportOpen(false)}>{t("correspondenceClose")}</Button>
|
||||
{importTab === 0 ? <Button variant="contained" onClick={importEmail}>{t("correspondenceLogEmail")}</Button> : null}
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</Box>
|
||||
|
||||
Reference in New Issue
Block a user