feat(ui): redesign job workspace sections

This commit is contained in:
cesnimda
2026-10-07 01:58:31 +02:00
parent acd1d94684
commit ac6d78289d
123 changed files with 2842 additions and 205 deletions
@@ -43,6 +43,7 @@ import {
} from "../types";
import { useDialogActions } from "../dialogs";
import { useI18n } from "../i18n/I18nProvider";
import ViewStateNotice from "./ViewStateNotice";
function parseRawEmail(raw: string): { subject?: string; date?: string; from?: string; to?: string; body: string } {
const lines = raw.replace(/\r\n/g, "\n").split("\n");
@@ -127,6 +128,8 @@ export default function Correspondence({ jobId, jobContext }: { jobId: number; j
return label;
};
const [messages, setMessages] = useState<CorrespondenceMessage[]>([]);
const [messagesLoading, setMessagesLoading] = useState(true);
const [messagesError, setMessagesError] = useState<string | null>(null);
const [from, setFrom] = useState<"Me" | "Company">("Me");
const [text, setText] = useState("");
const scrollRef = useRef<HTMLDivElement | null>(null);
@@ -154,9 +157,18 @@ export default function Correspondence({ jobId, jobContext }: { jobId: number; j
const autoRefreshKeyRef = useRef<string | null>(null);
const load = useCallback(async () => {
const res = await api.get<CorrespondenceMessage[]>(`/correspondence/${jobId}`);
setMessages(res.data);
}, [jobId]);
setMessagesLoading(true);
try {
const res = await api.get<CorrespondenceMessage[]>(`/correspondence/${jobId}`);
setMessages(res.data);
setMessagesError(null);
} catch (error) {
setMessagesError(getApiErrorMessage(error, t("correspondenceLoadFailed")));
throw error; // Existing mutation callers still handle their own operation failure.
} finally {
setMessagesLoading(false);
}
}, [jobId, t]);
const loadGmailStatus = useCallback(async () => {
try {
@@ -242,7 +254,7 @@ export default function Correspondence({ jobId, jobContext }: { jobId: number; j
}, [gmailQuery, gmailStatus?.connected, importOpen, importTab, jobId, linkedThreadIds.length, load, loadGmailMatches, loadGmailStatus, t, toast]);
useEffect(() => {
void load();
void load().catch(() => undefined);
}, [load]);
useEffect(() => {
@@ -460,8 +472,9 @@ export default function Correspondence({ jobId, jobContext }: { jobId: number; j
return (
<Box>
<Paper ref={scrollRef} sx={{ p: 1.5, maxHeight: 360, overflowY: "auto", background: theme.palette.mode === "dark" ? "rgba(15,23,42,0.45)" : "rgba(255,255,255,0.75)", backdropFilter: "blur(8px)" }}>
{messages.length === 0 ? (
<ViewStateNotice title={t("correspondenceLoadFailed")} loading={messagesLoading} error={messagesError ? { kind: "error", message: messagesError, retryable: true } : null} onRetry={() => load().catch(() => undefined)} compact />
<Paper variant="outlined" ref={scrollRef} sx={{ p: 1.5, maxHeight: 360, overflowY: "auto", bgcolor: "background.paper", borderRadius: 1 }}>
{messages.length === 0 && !messagesLoading && !messagesError ? (
<Typography sx={{ color: "text.secondary", py: 2, textAlign: "center" }}>{t("correspondenceNoMessages")}</Typography>
) : (
<Box sx={{ display: "flex", flexDirection: "column", gap: 1 }}>
@@ -470,7 +483,7 @@ export default function Correspondence({ jobId, jobContext }: { jobId: number; j
const accent = isMe ? theme.palette.primary.main : theme.palette.warning.main;
return (
<Box key={m.id} sx={{ display: "flex", justifyContent: isMe ? "flex-end" : "flex-start" }}>
<Box sx={{ maxWidth: "80%", borderRadius: 3, p: 1.25, border: `1px solid ${alpha(accent, theme.palette.mode === "dark" ? 0.32 : 0.22)}`, background: alpha(accent, theme.palette.mode === "dark" ? 0.14 : 0.1), color: "text.primary" }}>
<Box sx={{ maxWidth: { xs: "100%", sm: "85%" }, minWidth: 0, overflowWrap: "anywhere", borderRadius: 1, p: 1.25, border: "1px solid", borderColor: "divider", borderLeft: `3px solid ${accent}`, bgcolor: "background.default", color: "text.primary", "& .MuiChip-root": { maxWidth: "100%", height: "auto", minHeight: 24 }, "& .MuiChip-label": { whiteSpace: "normal", overflowWrap: "anywhere" } }}>
{m.subject ? <Typography sx={{ fontWeight: 800, mb: 0.5 }}>{m.subject}</Typography> : null}
<Typography sx={{ whiteSpace: "pre-wrap", lineHeight: 1.35 }}>{m.content}</Typography>
{(m.externalThreadId || m.externalFrom || m.externalTo || m.externalLabelsJson || m.attachmentMetadataJson) ? (
@@ -498,7 +511,7 @@ export default function Correspondence({ jobId, jobContext }: { jobId: number; j
)}
</Paper>
<Box sx={{ mt: 1.5, p: 1.5, borderRadius: 3, border: "none", boxShadow: "0px 1px 2px 0px rgba(15,23,42,0.04), 0px 8px 24px -12px rgba(15,23,42,0.12)", backgroundColor: "background.default" }}>
<Box sx={{ mt: 1.5, p: 1.5, borderRadius: 1, border: 1, borderColor: "divider", bgcolor: "background.default", "& .MuiChip-root": { maxWidth: "100%", height: "auto", minHeight: 24 }, "& .MuiChip-label": { whiteSpace: "normal", overflowWrap: "anywhere" } }}>
<Typography variant="overline">{t("correspondenceLinkedContinuity")}</Typography>
<Typography variant="body2" sx={{ color: "text.secondary", mb: 1.25 }}>
{t("correspondenceLinkedContinuityHelp")}
@@ -548,7 +561,7 @@ export default function Correspondence({ jobId, jobContext }: { jobId: number; j
<Button variant="outlined" size="small" onClick={() => setImportOpen(true)}>{t("correspondenceImportEmail")}</Button>
<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 })} />
<TextField label={t("correspondenceLogNoteOrMessage")} value={text} onChange={(e) => setText(e.target.value)} multiline minRows={3} sx={{ flex: { xs: "1 1 100%", sm: "1 1 320px" }, minWidth: 0 }} helperText={t("correspondenceCharacters", { count: text.length })} />
<Button variant="contained" onClick={send} disabled={!canSend}>{t("correspondenceAdd")}</Button>
</Box>