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 { useToast } from "../toast";
|
||||||
import { CorrespondenceMessage, GmailMessageSummary, GmailStatus } from "../types";
|
import { CorrespondenceMessage, GmailMessageSummary, GmailStatus } from "../types";
|
||||||
import { useDialogActions } from "../dialogs";
|
import { useDialogActions } from "../dialogs";
|
||||||
|
import { useI18n } from "../i18n/I18nProvider";
|
||||||
|
|
||||||
function parseRawEmail(raw: string): { subject?: string; date?: string; from?: string; to?: string; body: string } {
|
function parseRawEmail(raw: string): { subject?: string; date?: string; from?: string; to?: string; body: string } {
|
||||||
const lines = raw.replace(/\r\n/g, "\n").split("\n");
|
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 }) {
|
export default function Correspondence({ jobId }: { jobId: number }) {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
const { t } = useI18n();
|
||||||
const { confirmAction } = useDialogActions();
|
const { confirmAction } = useDialogActions();
|
||||||
const [messages, setMessages] = useState<CorrespondenceMessage[]>([]);
|
const [messages, setMessages] = useState<CorrespondenceMessage[]>([]);
|
||||||
const [from, setFrom] = useState<"Me" | "Company">("Me");
|
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 };
|
const data = event.data as { source?: string; status?: string; message?: string };
|
||||||
if (data?.source !== "jobtracker-gmail-oauth") return;
|
if (data?.source !== "jobtracker-gmail-oauth") return;
|
||||||
if (data.status === "connected") {
|
if (data.status === "connected") {
|
||||||
toast(data.message || "Gmail connected.", "success");
|
toast(data.message || t("googleLinkedSuccess"), "success");
|
||||||
void loadGmailStatus();
|
void loadGmailStatus();
|
||||||
setImportTab(1);
|
setImportTab(1);
|
||||||
void loadGmailMessages();
|
void loadGmailMessages();
|
||||||
} else {
|
} else {
|
||||||
toast(data.message || "Gmail connection failed.", "error");
|
toast(data.message || t("googleAuthFailed"), "error");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
window.addEventListener("message", onMessage);
|
window.addEventListener("message", onMessage);
|
||||||
return () => window.removeEventListener("message", onMessage);
|
return () => window.removeEventListener("message", onMessage);
|
||||||
}, [loadGmailMessages, loadGmailStatus, toast]);
|
}, [loadGmailMessages, loadGmailStatus, t, toast]);
|
||||||
|
|
||||||
const canSend = useMemo(() => text.trim().length > 0, [text]);
|
const canSend = useMemo(() => text.trim().length > 0, [text]);
|
||||||
|
|
||||||
@@ -213,7 +215,7 @@ export default function Correspondence({ jobId }: { jobId: number }) {
|
|||||||
const importEmail = async () => {
|
const importEmail = async () => {
|
||||||
const parsed = parseRawEmail(rawEmail);
|
const parsed = parseRawEmail(rawEmail);
|
||||||
if (!parsed.body && !parsed.subject && !rawEmail.trim()) {
|
if (!parsed.body && !parsed.subject && !rawEmail.trim()) {
|
||||||
toast("Paste an email first.", "error");
|
toast(t("addJobModalPasteUrlFirst"), "error");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -229,7 +231,7 @@ export default function Correspondence({ jobId }: { jobId: number }) {
|
|||||||
setImportOpen(false);
|
setImportOpen(false);
|
||||||
setRawEmail("");
|
setRawEmail("");
|
||||||
await load();
|
await load();
|
||||||
toast("Email logged.", "success");
|
toast(t("correspondenceLogEmail"), "success");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast(getApiErrorMessage(error, "Failed to import email."), "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");
|
await api.delete("/gmail/connection");
|
||||||
setGmailStatus({ connected: false });
|
setGmailStatus({ connected: false });
|
||||||
setGmailMessages([]);
|
setGmailMessages([]);
|
||||||
toast("Gmail disconnected.", "success");
|
toast(t("googleUnlinked"), "success");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast(getApiErrorMessage(error, "Failed to disconnect Gmail."), "error");
|
toast(getApiErrorMessage(error, "Failed to disconnect Gmail."), "error");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteMessage = async (messageId: number) => {
|
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 {
|
try {
|
||||||
await api.delete(`/correspondence/${messageId}`);
|
await api.delete(`/correspondence/${messageId}`);
|
||||||
await load();
|
await load();
|
||||||
@@ -272,7 +274,7 @@ export default function Correspondence({ jobId }: { jobId: number }) {
|
|||||||
setImportingMessageId(messageId);
|
setImportingMessageId(messageId);
|
||||||
await api.post("/gmail/import", { jobApplicationId: jobId, messageId });
|
await api.post("/gmail/import", { jobApplicationId: jobId, messageId });
|
||||||
await load();
|
await load();
|
||||||
toast("Email imported from Gmail.", "success");
|
toast(t("correspondenceImportEmail"), "success");
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
toast(getApiErrorMessage(error, "Failed to import Gmail message."), "error");
|
toast(getApiErrorMessage(error, "Failed to import Gmail message."), "error");
|
||||||
} finally {
|
} finally {
|
||||||
@@ -310,7 +312,7 @@ export default function Correspondence({ jobId }: { jobId: number }) {
|
|||||||
<Typography sx={{ whiteSpace: "pre-wrap", lineHeight: 1.35 }}>{m.content}</Typography>
|
<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 }}>
|
<Box sx={{ display: "flex", justifyContent: "space-between", gap: 1, alignItems: "flex-end", mt: 0.75 }}>
|
||||||
<Typography variant="caption" sx={{ color: "text.secondary" }}>
|
<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>
|
</Typography>
|
||||||
<IconButton size="small" onClick={() => void deleteMessage(m.id)} sx={{ color: "text.secondary" }}>
|
<IconButton size="small" onClick={() => void deleteMessage(m.id)} sx={{ color: "text.secondary" }}>
|
||||||
<DeleteOutlineIcon fontSize="small" />
|
<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" }}>
|
<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">
|
<ToggleButtonGroup exclusive value={from} onChange={(_, v) => v && setFrom(v)} size="small">
|
||||||
<ToggleButton value="Me">Me</ToggleButton>
|
<ToggleButton value="Me">{t("correspondenceMe")}</ToggleButton>
|
||||||
<ToggleButton value="Company">Company</ToggleButton>
|
<ToggleButton value="Company">{t("correspondenceCompany")}</ToggleButton>
|
||||||
</ToggleButtonGroup>
|
</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>
|
</Box>
|
||||||
|
|
||||||
<Dialog open={importOpen} onClose={() => setImportOpen(false)} fullWidth maxWidth="md">
|
<Dialog open={importOpen} onClose={() => setImportOpen(false)} fullWidth maxWidth="md">
|
||||||
<DialogTitle>Import Email</DialogTitle>
|
<DialogTitle>{t("correspondenceImportTitle")}</DialogTitle>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<Tabs value={importTab} onChange={(_, v) => setImportTab(v)} sx={{ mb: 2 }}>
|
<Tabs value={importTab} onChange={(_, v) => setImportTab(v)} sx={{ mb: 2 }}>
|
||||||
<Tab label="Paste email" />
|
<Tab label={t("correspondencePasteEmail")} />
|
||||||
<Tab label="Gmail" />
|
<Tab label={t("google")} />
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
{importTab === 0 ? (
|
{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..."} />
|
<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", flexDirection: "column", gap: 2 }}>
|
||||||
<Box sx={{ display: "flex", justifyContent: "space-between", gap: 1, flexWrap: "wrap", alignItems: "center" }}>
|
<Box sx={{ display: "flex", justifyContent: "space-between", gap: 1, flexWrap: "wrap", alignItems: "center" }}>
|
||||||
<Box>
|
<Box>
|
||||||
<Typography sx={{ fontWeight: 800 }}>Google Gmail</Typography>
|
<Typography sx={{ fontWeight: 800 }}>{t("correspondenceGoogleGmail")}</Typography>
|
||||||
<Typography variant="body2" sx={{ color: "text.secondary" }}>
|
<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>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
<Box sx={{ display: "flex", gap: 1, flexWrap: "wrap" }}>
|
<Box sx={{ display: "flex", gap: 1, flexWrap: "wrap" }}>
|
||||||
{gmailStatus?.connected ? (
|
{gmailStatus?.connected ? (
|
||||||
<>
|
<>
|
||||||
<Button variant="outlined" onClick={() => void loadGmailMessages()} disabled={gmailMessagesLoading}>Refresh</Button>
|
<Button variant="outlined" onClick={() => void loadGmailMessages()} disabled={gmailMessagesLoading}>{t("correspondenceRefresh")}</Button>
|
||||||
<Button variant="outlined" color="error" onClick={() => void disconnectGmail()}>Disconnect</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>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -379,8 +381,8 @@ export default function Correspondence({ jobId }: { jobId: number }) {
|
|||||||
))}
|
))}
|
||||||
</Box>
|
</Box>
|
||||||
<Box sx={{ display: "flex", gap: 1, flexWrap: "wrap", alignItems: "center" }}>
|
<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 />
|
<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}>Search</Button>
|
<Button variant="outlined" onClick={() => void loadGmailMessages()} disabled={gmailMessagesLoading}>{t("correspondenceSearch")}</Button>
|
||||||
</Box>
|
</Box>
|
||||||
{gmailStatus.lastSyncedAt ? <Chip label={`Last synced ${new Date(gmailStatus.lastSyncedAt).toLocaleString()}`} size="small" /> : null}
|
{gmailStatus.lastSyncedAt ? <Chip label={`Last synced ${new Date(gmailStatus.lastSyncedAt).toLocaleString()}`} size="small" /> : null}
|
||||||
<Paper variant="outlined" sx={{ maxHeight: 420, overflowY: "auto" }}>
|
<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 }}>
|
<ListItemButton sx={{ alignItems: "flex-start", px: 0, py: 1 }}>
|
||||||
<ListItemText
|
<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>}
|
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)}>
|
<Button variant="contained" size="small" disabled={importingMessageId === message.id} onClick={() => void importGmailMessage(message.id)}>
|
||||||
{importingMessageId === message.id ? "Importing..." : "Import"}
|
{importingMessageId === message.id ? "Importing..." : "Import"}
|
||||||
@@ -429,8 +431,8 @@ export default function Correspondence({ jobId }: { jobId: number }) {
|
|||||||
)}
|
)}
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button onClick={() => setImportOpen(false)}>Close</Button>
|
<Button onClick={() => setImportOpen(false)}>{t("correspondenceClose")}</Button>
|
||||||
{importTab === 0 ? <Button variant="contained" onClick={importEmail}>Log Email</Button> : null}
|
{importTab === 0 ? <Button variant="contained" onClick={importEmail}>{t("correspondenceLogEmail")}</Button> : null}
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -277,6 +277,75 @@ export const translations = {
|
|||||||
adminSystemRunningProbe: "Running probe...",
|
adminSystemRunningProbe: "Running probe...",
|
||||||
adminSystemRefresh: "Refresh",
|
adminSystemRefresh: "Refresh",
|
||||||
adminSystemRefreshing: "Refreshing...",
|
adminSystemRefreshing: "Refreshing...",
|
||||||
|
adminSystemProvider: "Provider",
|
||||||
|
adminSystemTarget: "Target",
|
||||||
|
adminSystemConfigured: "Configured",
|
||||||
|
adminSystemCanConnect: "Can connect",
|
||||||
|
adminSystemUsesFileStorage: "Uses file storage",
|
||||||
|
adminSystemDataRoot: "Data root",
|
||||||
|
adminSystemDbPath: "DB path",
|
||||||
|
adminSystemDbFileExists: "DB file exists",
|
||||||
|
adminSystemDbSize: "DB size",
|
||||||
|
adminSystemJobs: "Jobs",
|
||||||
|
adminSystemDeletedJobs: "Deleted jobs",
|
||||||
|
adminSystemFramework: "Framework",
|
||||||
|
adminSystemOs: "OS",
|
||||||
|
adminSystemArchitecture: "Architecture",
|
||||||
|
adminSystemMachine: "Machine",
|
||||||
|
adminSystemContentRoot: "Content root",
|
||||||
|
adminSystemBuildStamp: "Build stamp",
|
||||||
|
adminSystemAuthRequired: "Auth required",
|
||||||
|
adminSystemJwtConfigured: "JWT key configured",
|
||||||
|
adminSystemGoogleConfigured: "Google login configured",
|
||||||
|
adminSystemGmailConfigured: "Gmail integration configured",
|
||||||
|
adminSystemFrom: "From",
|
||||||
|
adminSystemFromName: "From name",
|
||||||
|
adminSystemHost: "Host",
|
||||||
|
adminSystemPort: "Port",
|
||||||
|
adminSystemSsl: "SSL",
|
||||||
|
adminSystemModel: "Model",
|
||||||
|
adminSystemDevice: "Device",
|
||||||
|
adminSystemGpuAvailable: "GPU available",
|
||||||
|
adminSystemGpuName: "GPU name",
|
||||||
|
adminSystemHealthLatency: "Health latency",
|
||||||
|
adminSystemProbeLatency: "Probe latency",
|
||||||
|
adminSystemLastProbe: "Last probe",
|
||||||
|
adminSystemLastSuccessfulProbe: "Last successful probe",
|
||||||
|
adminSystemLastSummarizationSuccess: "Last summarization success",
|
||||||
|
adminSystemRequests: "Requests",
|
||||||
|
adminSystemCacheHits: "Cache hits",
|
||||||
|
adminSystemCacheMisses: "Cache misses",
|
||||||
|
adminSystemFailures: "Failures",
|
||||||
|
adminSystemProbeFailures: "Probe failures",
|
||||||
|
adminSystemAvgLatency: "Avg latency",
|
||||||
|
adminSystemOcrRequests: "OCR requests",
|
||||||
|
adminSystemOcrAvgLatency: "OCR avg latency",
|
||||||
|
adminSystemOcrUnavailable: "OCR unavailable",
|
||||||
|
adminSystemAiProbeFailed: "Failed to run AI service probe.",
|
||||||
|
correspondenceNoMessages: "No messages yet.",
|
||||||
|
correspondenceMe: "Me",
|
||||||
|
correspondenceCompany: "Company",
|
||||||
|
correspondenceImportEmail: "Import email",
|
||||||
|
correspondenceLogNoteOrMessage: "Log note or message",
|
||||||
|
correspondenceCharacters: "{count} characters",
|
||||||
|
correspondenceAdd: "Add",
|
||||||
|
correspondenceImportTitle: "Import email",
|
||||||
|
correspondencePasteEmail: "Paste email",
|
||||||
|
correspondencePasteEmailHelp: "Paste raw email text (headers optional). We parse Subject and Date when present.",
|
||||||
|
correspondenceGoogleGmail: "Google Gmail",
|
||||||
|
correspondenceCheckingConnection: "Checking connection...",
|
||||||
|
correspondenceConnectedAs: "Connected as {email}",
|
||||||
|
correspondenceConnectGmailHint: "Connect your Gmail account to browse recent emails.",
|
||||||
|
correspondenceRefresh: "Refresh",
|
||||||
|
correspondenceDisconnect: "Disconnect",
|
||||||
|
correspondenceConnectGmail: "Connect Gmail",
|
||||||
|
correspondenceSearchGmail: "Search Gmail",
|
||||||
|
correspondenceSearchGmailPlaceholder: "from:company@example.com OR interview",
|
||||||
|
correspondenceSearch: "Search",
|
||||||
|
correspondenceNoGmailMessages: "No Gmail messages found.",
|
||||||
|
correspondenceUnknown: "Unknown",
|
||||||
|
correspondenceClose: "Close",
|
||||||
|
correspondenceLogEmail: "Log email",
|
||||||
adminSystemEnvironment: "Environment",
|
adminSystemEnvironment: "Environment",
|
||||||
adminSystemDatabase: "Database",
|
adminSystemDatabase: "Database",
|
||||||
adminSystemConnected: "Connected",
|
adminSystemConnected: "Connected",
|
||||||
@@ -697,6 +766,75 @@ export const translations = {
|
|||||||
adminSystemRunningProbe: "Kjører probe...",
|
adminSystemRunningProbe: "Kjører probe...",
|
||||||
adminSystemRefresh: "Oppdater",
|
adminSystemRefresh: "Oppdater",
|
||||||
adminSystemRefreshing: "Oppdaterer...",
|
adminSystemRefreshing: "Oppdaterer...",
|
||||||
|
adminSystemProvider: "Leverandør",
|
||||||
|
adminSystemTarget: "Mål",
|
||||||
|
adminSystemConfigured: "Konfigurert",
|
||||||
|
adminSystemCanConnect: "Kan koble til",
|
||||||
|
adminSystemUsesFileStorage: "Bruker fillagring",
|
||||||
|
adminSystemDataRoot: "Datarot",
|
||||||
|
adminSystemDbPath: "DB-sti",
|
||||||
|
adminSystemDbFileExists: "DB-fil finnes",
|
||||||
|
adminSystemDbSize: "DB-størrelse",
|
||||||
|
adminSystemJobs: "Jobber",
|
||||||
|
adminSystemDeletedJobs: "Slettede jobber",
|
||||||
|
adminSystemFramework: "Rammeverk",
|
||||||
|
adminSystemOs: "OS",
|
||||||
|
adminSystemArchitecture: "Arkitektur",
|
||||||
|
adminSystemMachine: "Maskin",
|
||||||
|
adminSystemContentRoot: "Innholdsrot",
|
||||||
|
adminSystemBuildStamp: "Byggestempel",
|
||||||
|
adminSystemAuthRequired: "Autentisering kreves",
|
||||||
|
adminSystemJwtConfigured: "JWT-nøkkel konfigurert",
|
||||||
|
adminSystemGoogleConfigured: "Google-innlogging konfigurert",
|
||||||
|
adminSystemGmailConfigured: "Gmail-integrasjon konfigurert",
|
||||||
|
adminSystemFrom: "Fra",
|
||||||
|
adminSystemFromName: "Fra-navn",
|
||||||
|
adminSystemHost: "Vert",
|
||||||
|
adminSystemPort: "Port",
|
||||||
|
adminSystemSsl: "SSL",
|
||||||
|
adminSystemModel: "Modell",
|
||||||
|
adminSystemDevice: "Enhet",
|
||||||
|
adminSystemGpuAvailable: "GPU tilgjengelig",
|
||||||
|
adminSystemGpuName: "GPU-navn",
|
||||||
|
adminSystemHealthLatency: "Helselatens",
|
||||||
|
adminSystemProbeLatency: "Probelatens",
|
||||||
|
adminSystemLastProbe: "Siste probe",
|
||||||
|
adminSystemLastSuccessfulProbe: "Siste vellykkede probe",
|
||||||
|
adminSystemLastSummarizationSuccess: "Siste vellykkede oppsummering",
|
||||||
|
adminSystemRequests: "Forespørsler",
|
||||||
|
adminSystemCacheHits: "Cache-treff",
|
||||||
|
adminSystemCacheMisses: "Cache-miss",
|
||||||
|
adminSystemFailures: "Feil",
|
||||||
|
adminSystemProbeFailures: "Probefeil",
|
||||||
|
adminSystemAvgLatency: "Snittlatens",
|
||||||
|
adminSystemOcrRequests: "OCR-forespørsler",
|
||||||
|
adminSystemOcrAvgLatency: "OCR snittlatens",
|
||||||
|
adminSystemOcrUnavailable: "OCR utilgjengelig",
|
||||||
|
adminSystemAiProbeFailed: "Kunne ikke kjøre AI-tjenesteprobe.",
|
||||||
|
correspondenceNoMessages: "Ingen meldinger ennå.",
|
||||||
|
correspondenceMe: "Meg",
|
||||||
|
correspondenceCompany: "Selskap",
|
||||||
|
correspondenceImportEmail: "Importer e-post",
|
||||||
|
correspondenceLogNoteOrMessage: "Loggfør notat eller melding",
|
||||||
|
correspondenceCharacters: "{count} tegn",
|
||||||
|
correspondenceAdd: "Legg til",
|
||||||
|
correspondenceImportTitle: "Importer e-post",
|
||||||
|
correspondencePasteEmail: "Lim inn e-post",
|
||||||
|
correspondencePasteEmailHelp: "Lim inn rå e-posttekst (overskrifter valgfritt). Vi tolker emne og dato når de finnes.",
|
||||||
|
correspondenceGoogleGmail: "Google Gmail",
|
||||||
|
correspondenceCheckingConnection: "Sjekker tilkobling...",
|
||||||
|
correspondenceConnectedAs: "Tilkoblet som {email}",
|
||||||
|
correspondenceConnectGmailHint: "Koble til Gmail-kontoen din for å bla gjennom nylige e-poster.",
|
||||||
|
correspondenceRefresh: "Oppdater",
|
||||||
|
correspondenceDisconnect: "Koble fra",
|
||||||
|
correspondenceConnectGmail: "Koble til Gmail",
|
||||||
|
correspondenceSearchGmail: "Søk i Gmail",
|
||||||
|
correspondenceSearchGmailPlaceholder: "from:company@example.com OR interview",
|
||||||
|
correspondenceSearch: "Søk",
|
||||||
|
correspondenceNoGmailMessages: "Ingen Gmail-meldinger funnet.",
|
||||||
|
correspondenceUnknown: "Ukjent",
|
||||||
|
correspondenceClose: "Lukk",
|
||||||
|
correspondenceLogEmail: "Loggfør e-post",
|
||||||
adminSystemEnvironment: "Miljø",
|
adminSystemEnvironment: "Miljø",
|
||||||
adminSystemDatabase: "Database",
|
adminSystemDatabase: "Database",
|
||||||
adminSystemConnected: "Tilkoblet",
|
adminSystemConnected: "Tilkoblet",
|
||||||
|
|||||||
@@ -247,34 +247,34 @@ export default function AdminSystemPage() {
|
|||||||
<Paper sx={{ p: 2, borderRadius: 3 }}>
|
<Paper sx={{ p: 2, borderRadius: 3 }}>
|
||||||
<Typography variant="h6" sx={{ fontWeight: 900, mb: 1 }}>{t("adminSystemDatabaseStorage")}</Typography>
|
<Typography variant="h6" sx={{ fontWeight: 900, mb: 1 }}>{t("adminSystemDatabaseStorage")}</Typography>
|
||||||
<Stack spacing={0.75}>
|
<Stack spacing={0.75}>
|
||||||
<DetailRow label="Provider" value={status?.database.provider || "-"} />
|
<DetailRow label={t("adminSystemProvider")} value={status?.database.provider || "-"} />
|
||||||
<DetailRow label="Target" value={status?.database.target || "-"} />
|
<DetailRow label={t("adminSystemTarget")} value={status?.database.target || "-"} />
|
||||||
<DetailRow label="Configured" value={status?.database.looksConfigured ? "Yes" : "No"} />
|
<DetailRow label={t("adminSystemConfigured")} value={status?.database.looksConfigured ? "Yes" : "No"} />
|
||||||
<DetailRow label="Can connect" value={status?.database.canConnect ? "Yes" : "No"} />
|
<DetailRow label={t("adminSystemCanConnect")} value={status?.database.canConnect ? "Yes" : "No"} />
|
||||||
<DetailRow label="Uses file storage" value={status?.database.usesFileStorage ? "Yes" : "No"} />
|
<DetailRow label={t("adminSystemUsesFileStorage")} value={status?.database.usesFileStorage ? "Yes" : "No"} />
|
||||||
<DetailRow label="Data root" value={status?.storage.dataRoot || "-"} />
|
<DetailRow label={t("adminSystemDataRoot")} value={status?.storage.dataRoot || "-"} />
|
||||||
<DetailRow label="DB path" value={status?.storage.dbPath || "-"} />
|
<DetailRow label={t("adminSystemDbPath")} value={status?.storage.dbPath || "-"} />
|
||||||
<DetailRow label="DB file exists" value={status?.storage.dbExists ? "Yes" : "No"} />
|
<DetailRow label={t("adminSystemDbFileExists")} value={status?.storage.dbExists ? "Yes" : "No"} />
|
||||||
<DetailRow label="DB size" value={formatBytes(status?.storage.dbSizeBytes)} />
|
<DetailRow label={t("adminSystemDbSize")} value={formatBytes(status?.storage.dbSizeBytes)} />
|
||||||
<DetailRow label="Companies" value={status?.storage.companyCount ?? 0} />
|
<DetailRow label="Companies" value={status?.storage.companyCount ?? 0} />
|
||||||
<DetailRow label="Jobs" value={status?.storage.jobCount ?? 0} />
|
<DetailRow label={t("adminSystemJobs")} value={status?.storage.jobCount ?? 0} />
|
||||||
<DetailRow label="Deleted jobs" value={status?.storage.deletedCount ?? 0} />
|
<DetailRow label={t("adminSystemDeletedJobs")} value={status?.storage.deletedCount ?? 0} />
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|
||||||
<Paper sx={{ p: 2, borderRadius: 3 }}>
|
<Paper sx={{ p: 2, borderRadius: 3 }}>
|
||||||
<Typography variant="h6" sx={{ fontWeight: 900, mb: 1 }}>{t("adminSystemRuntimeAuth")}</Typography>
|
<Typography variant="h6" sx={{ fontWeight: 900, mb: 1 }}>{t("adminSystemRuntimeAuth")}</Typography>
|
||||||
<Stack spacing={0.75}>
|
<Stack spacing={0.75}>
|
||||||
<DetailRow label="Framework" value={status?.runtime.framework || "-"} />
|
<DetailRow label={t("adminSystemFramework")} value={status?.runtime.framework || "-"} />
|
||||||
<DetailRow label="OS" value={status?.runtime.osDescription || "-"} />
|
<DetailRow label={t("adminSystemOs")} value={status?.runtime.osDescription || "-"} />
|
||||||
<DetailRow label="Architecture" value={status?.runtime.processArchitecture || "-"} />
|
<DetailRow label={t("adminSystemArchitecture")} value={status?.runtime.processArchitecture || "-"} />
|
||||||
<DetailRow label="Machine" value={status?.runtime.machineName || "-"} />
|
<DetailRow label={t("adminSystemMachine")} value={status?.runtime.machineName || "-"} />
|
||||||
<DetailRow label="Content root" value={status?.contentRoot || "-"} />
|
<DetailRow label={t("adminSystemContentRoot")} value={status?.contentRoot || "-"} />
|
||||||
<DetailRow label="Build stamp" value={displayMetadata(status?.buildStamp)} />
|
<DetailRow label={t("adminSystemBuildStamp")} value={displayMetadata(status?.buildStamp)} />
|
||||||
<DetailRow label="Auth required" value={status?.auth.required ? "Yes" : "No"} />
|
<DetailRow label={t("adminSystemAuthRequired")} value={status?.auth.required ? "Yes" : "No"} />
|
||||||
<DetailRow label="JWT key configured" value={status?.auth.hasJwtKey ? "Yes" : "No"} />
|
<DetailRow label={t("adminSystemJwtConfigured")} value={status?.auth.hasJwtKey ? "Yes" : "No"} />
|
||||||
<DetailRow label="Google login configured" value={status?.auth.googleConfigured ? "Yes" : "No"} />
|
<DetailRow label={t("adminSystemGoogleConfigured")} value={status?.auth.googleConfigured ? "Yes" : "No"} />
|
||||||
<DetailRow label="Gmail integration configured" value={status?.auth.gmailConfigured ? "Yes" : "No"} />
|
<DetailRow label={t("adminSystemGmailConfigured")} value={status?.auth.gmailConfigured ? "Yes" : "No"} />
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -283,27 +283,27 @@ export default function AdminSystemPage() {
|
|||||||
<Paper sx={{ p: 2, borderRadius: 3 }}>
|
<Paper sx={{ p: 2, borderRadius: 3 }}>
|
||||||
<Typography variant="h6" sx={{ fontWeight: 900, mb: 1 }}>{t("adminSystemEmailConfig")}</Typography>
|
<Typography variant="h6" sx={{ fontWeight: 900, mb: 1 }}>{t("adminSystemEmailConfig")}</Typography>
|
||||||
<Stack spacing={0.75}>
|
<Stack spacing={0.75}>
|
||||||
<DetailRow label="Enabled" value={status?.email.enabled ? "Yes" : "No"} />
|
<DetailRow label={t("adminSystemEnabled")} value={status?.email.enabled ? "Yes" : "No"} />
|
||||||
<DetailRow label="From" value={status?.email.from || "-"} />
|
<DetailRow label={t("adminSystemFrom")} value={status?.email.from || "-"} />
|
||||||
<DetailRow label="From name" value={status?.email.fromName || "-"} />
|
<DetailRow label={t("adminSystemFromName")} value={status?.email.fromName || "-"} />
|
||||||
<DetailRow label="Host" value={status?.email.host || "-"} />
|
<DetailRow label={t("adminSystemHost")} value={status?.email.host || "-"} />
|
||||||
<DetailRow label="Port" value={status?.email.port ?? "-"} />
|
<DetailRow label={t("adminSystemPort")} value={status?.email.port ?? "-"} />
|
||||||
<DetailRow label="SSL" value={status?.email.enableSsl ? "Yes" : "No"} />
|
<DetailRow label={t("adminSystemSsl")} value={status?.email.enableSsl ? "Yes" : "No"} />
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|
||||||
<Paper sx={{ p: 2, borderRadius: 3 }}>
|
<Paper sx={{ p: 2, borderRadius: 3 }}>
|
||||||
<Typography variant="h6" sx={{ fontWeight: 900, mb: 1 }}>{t("adminSystemSummarizerRuntime")}</Typography>
|
<Typography variant="h6" sx={{ fontWeight: 900, mb: 1 }}>{t("adminSystemSummarizerRuntime")}</Typography>
|
||||||
<Stack spacing={0.75}>
|
<Stack spacing={0.75}>
|
||||||
<DetailRow label="Model" value={status?.ai.model || "-"} />
|
<DetailRow label={t("adminSystemModel")} value={status?.ai.model || "-"} />
|
||||||
<DetailRow label="Device" value={status?.ai.device || "-"} />
|
<DetailRow label={t("adminSystemDevice")} value={status?.ai.device || "-"} />
|
||||||
<DetailRow label="GPU available" value={status?.ai.gpuAvailable ? "Yes" : "No"} />
|
<DetailRow label={t("adminSystemGpuAvailable")} value={status?.ai.gpuAvailable ? "Yes" : "No"} />
|
||||||
<DetailRow label="GPU name" value={status?.ai.gpuName || "-"} />
|
<DetailRow label={t("adminSystemGpuName")} value={status?.ai.gpuName || "-"} />
|
||||||
<DetailRow label="Health latency" value={status?.ai.healthLatencyMs != null ? `${status.ai.healthLatencyMs} ms` : "-"} />
|
<DetailRow label={t("adminSystemHealthLatency")} value={status?.ai.healthLatencyMs != null ? `${status.ai.healthLatencyMs} ms` : "-"} />
|
||||||
<DetailRow label="Probe latency" value={status?.ai.probeLatencyMs != null ? `${status.ai.probeLatencyMs} ms` : "-"} />
|
<DetailRow label={t("adminSystemProbeLatency")} value={status?.ai.probeLatencyMs != null ? `${status.ai.probeLatencyMs} ms` : "-"} />
|
||||||
<DetailRow label="Last probe" value={formatDate(status?.ai.lastProbeAt)} />
|
<DetailRow label={t("adminSystemLastProbe")} value={formatDate(status?.ai.lastProbeAt)} />
|
||||||
<DetailRow label="Last successful probe" value={formatDate(status?.ai.lastProbeSuccessAt)} />
|
<DetailRow label={t("adminSystemLastSuccessfulProbe")} value={formatDate(status?.ai.lastProbeSuccessAt)} />
|
||||||
<DetailRow label="Last summarization success" value={formatDate(status?.ai.lastSuccessAt)} />
|
<DetailRow label={t("adminSystemLastSummarizationSuccess")} value={formatDate(status?.ai.lastSuccessAt)} />
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -328,14 +328,14 @@ export default function AdminSystemPage() {
|
|||||||
<Paper sx={{ p: 2, borderRadius: 3 }}>
|
<Paper sx={{ p: 2, borderRadius: 3 }}>
|
||||||
<Typography variant="h6" sx={{ fontWeight: 900, mb: 1 }}>{t("adminSystemSummarizerTelemetry")}</Typography>
|
<Typography variant="h6" sx={{ fontWeight: 900, mb: 1 }}>{t("adminSystemSummarizerTelemetry")}</Typography>
|
||||||
<Box sx={{ display: "grid", gridTemplateColumns: { xs: "1fr 1fr", md: "repeat(8, 1fr)" }, gap: 2 }}>
|
<Box sx={{ display: "grid", gridTemplateColumns: { xs: "1fr 1fr", md: "repeat(8, 1fr)" }, gap: 2 }}>
|
||||||
<Box><Typography variant="overline" sx={{ color: "text.secondary" }}>Requests</Typography><Typography variant="h6" sx={{ fontWeight: 900 }}>{status?.ai.requests ?? 0}</Typography></Box>
|
<Box><Typography variant="overline" sx={{ color: "text.secondary" }}>{t("adminSystemRequests")}</Typography><Typography variant="h6" sx={{ fontWeight: 900 }}>{status?.ai.requests ?? 0}</Typography></Box>
|
||||||
<Box><Typography variant="overline" sx={{ color: "text.secondary" }}>Cache hits</Typography><Typography variant="h6" sx={{ fontWeight: 900 }}>{status?.ai.cacheHits ?? 0}</Typography></Box>
|
<Box><Typography variant="overline" sx={{ color: "text.secondary" }}>{t("adminSystemCacheHits")}</Typography><Typography variant="h6" sx={{ fontWeight: 900 }}>{status?.ai.cacheHits ?? 0}</Typography></Box>
|
||||||
<Box><Typography variant="overline" sx={{ color: "text.secondary" }}>Cache misses</Typography><Typography variant="h6" sx={{ fontWeight: 900 }}>{status?.ai.cacheMisses ?? 0}</Typography></Box>
|
<Box><Typography variant="overline" sx={{ color: "text.secondary" }}>{t("adminSystemCacheMisses")}</Typography><Typography variant="h6" sx={{ fontWeight: 900 }}>{status?.ai.cacheMisses ?? 0}</Typography></Box>
|
||||||
<Box><Typography variant="overline" sx={{ color: "text.secondary" }}>Failures</Typography><Typography variant="h6" sx={{ fontWeight: 900 }}>{status?.ai.failures ?? 0}</Typography></Box>
|
<Box><Typography variant="overline" sx={{ color: "text.secondary" }}>{t("adminSystemFailures")}</Typography><Typography variant="h6" sx={{ fontWeight: 900 }}>{status?.ai.failures ?? 0}</Typography></Box>
|
||||||
<Box><Typography variant="overline" sx={{ color: "text.secondary" }}>Probe failures</Typography><Typography variant="h6" sx={{ fontWeight: 900 }}>{status?.ai.probeFailures ?? 0}</Typography></Box>
|
<Box><Typography variant="overline" sx={{ color: "text.secondary" }}>{t("adminSystemProbeFailures")}</Typography><Typography variant="h6" sx={{ fontWeight: 900 }}>{status?.ai.probeFailures ?? 0}</Typography></Box>
|
||||||
<Box><Typography variant="overline" sx={{ color: "text.secondary" }}>Avg latency</Typography><Typography variant="h6" sx={{ fontWeight: 900 }}>{status?.ai.averageLatencyMs != null ? `${status.ai.averageLatencyMs} ms` : "-"}</Typography></Box>
|
<Box><Typography variant="overline" sx={{ color: "text.secondary" }}>{t("adminSystemAvgLatency")}</Typography><Typography variant="h6" sx={{ fontWeight: 900 }}>{status?.ai.averageLatencyMs != null ? `${status.ai.averageLatencyMs} ms` : "-"}</Typography></Box>
|
||||||
<Box><Typography variant="overline" sx={{ color: "text.secondary" }}>OCR requests</Typography><Typography variant="h6" sx={{ fontWeight: 900 }}>{status?.ai.ocrRequests ?? 0}</Typography></Box>
|
<Box><Typography variant="overline" sx={{ color: "text.secondary" }}>{t("adminSystemOcrRequests")}</Typography><Typography variant="h6" sx={{ fontWeight: 900 }}>{status?.ai.ocrRequests ?? 0}</Typography></Box>
|
||||||
<Box><Typography variant="overline" sx={{ color: "text.secondary" }}>OCR avg latency</Typography><Typography variant="h6" sx={{ fontWeight: 900 }}>{status?.ai.averageOcrLatencyMs != null ? `${status.ai.averageOcrLatencyMs} ms` : "-"}</Typography></Box>
|
<Box><Typography variant="overline" sx={{ color: "text.secondary" }}>{t("adminSystemOcrAvgLatency")}</Typography><Typography variant="h6" sx={{ fontWeight: 900 }}>{status?.ai.averageOcrLatencyMs != null ? `${status.ai.averageOcrLatencyMs} ms` : "-"}</Typography></Box>
|
||||||
</Box>
|
</Box>
|
||||||
<Box sx={{ display: "flex", gap: 1, flexWrap: "wrap", mt: 2 }}>
|
<Box sx={{ display: "flex", gap: 1, flexWrap: "wrap", mt: 2 }}>
|
||||||
<Chip label={status?.database.canConnect ? t("adminSystemDatabaseConnected") : t("adminSystemDatabaseIssue")} color={status?.database.canConnect ? "success" : "error"} size="small" />
|
<Chip label={status?.database.canConnect ? t("adminSystemDatabaseConnected") : t("adminSystemDatabaseIssue")} color={status?.database.canConnect ? "success" : "error"} size="small" />
|
||||||
@@ -343,7 +343,7 @@ export default function AdminSystemPage() {
|
|||||||
<Chip label={status?.auth.googleConfigured ? t("adminSystemGoogleReady") : t("adminSystemGoogleOff")} variant="outlined" size="small" />
|
<Chip label={status?.auth.googleConfigured ? t("adminSystemGoogleReady") : t("adminSystemGoogleOff")} variant="outlined" size="small" />
|
||||||
<Chip label={status?.auth.gmailConfigured ? t("adminSystemGmailReady") : t("adminSystemGmailIncomplete")} variant="outlined" size="small" />
|
<Chip label={status?.auth.gmailConfigured ? t("adminSystemGmailReady") : t("adminSystemGmailIncomplete")} variant="outlined" size="small" />
|
||||||
<Chip label={status?.ai.gpuAvailable ? t("adminSystemGpuVisible") : t("adminSystemCpuMode")} color={status?.ai.gpuAvailable ? "success" : "default"} size="small" />
|
<Chip label={status?.ai.gpuAvailable ? t("adminSystemGpuVisible") : t("adminSystemCpuMode")} color={status?.ai.gpuAvailable ? "success" : "default"} size="small" />
|
||||||
<Chip label={status?.ai.ocrAvailable ? `OCR ${status.ai.ocrLanguages || "enabled"}` : "OCR unavailable"} variant="outlined" size="small" />
|
<Chip label={status?.ai.ocrAvailable ? `OCR ${status.ai.ocrLanguages || "enabled"}` : t("adminSystemOcrUnavailable")} variant="outlined" size="small" />
|
||||||
</Box>
|
</Box>
|
||||||
</Paper>
|
</Paper>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
Reference in New Issue
Block a user