Skip to content

Commit a7e90d8

Browse files
committed
refactor(components): extract useDraftIntake hook
Move caseIntake state plus its four handlers (field change, analyze, preset apply, note-audience change) and the INTAKE_PRESETS constant out of DraftTab.tsx into a dedicated hook. Shell passes input, currentTicket(Id), response, logEvent, and setWorkspacePersonalization in; hook owns the caseIntake state and returns setCaseIntake so useWorkspaceDraftState can still hydrate intake on draft load.
1 parent fe0d53b commit a7e90d8

3 files changed

Lines changed: 207 additions & 79 deletions

File tree

src/components/Draft/DraftTab.tsx

Lines changed: 17 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { ConversationThread, ConversationEntry } from "./ConversationThread";
1717
import { useDraftApproval } from "./useDraftApproval";
1818
import { useDraftChecklist } from "./useDraftChecklist";
1919
import { useDraftFirstResponse } from "./useDraftFirstResponse";
20+
import { useDraftIntake } from "./useDraftIntake";
2021
import { ConversationInput } from "./ConversationInput";
2122
import { WorkspaceDialogs } from "./WorkspaceDialogs";
2223
import { WorkspaceModeShell } from "./WorkspaceModeShell";
@@ -41,7 +42,6 @@ import { useWorkspaceCommandBridge } from "../../features/workspace/useWorkspace
4142
import { useWorkspaceDraftState } from "../../features/workspace/useWorkspaceDraftState";
4243
import {
4344
applyResolutionKit,
44-
analyzeCaseIntake,
4545
buildResolutionKitFromWorkspace,
4646
buildSimilarCases,
4747
compactLines,
@@ -66,10 +66,8 @@ import type {
6666
} from "../../types/llm";
6767
import type { ContextSource } from "../../types/knowledge";
6868
import type {
69-
CaseIntake,
7069
GuidedRunbookTemplate,
7170
NextActionRecommendation,
72-
NoteAudience,
7371
ResolutionKit,
7472
ResponseLength,
7573
SavedDraft,
@@ -205,32 +203,6 @@ function isEditableTarget(target: EventTarget | null): boolean {
205203
);
206204
}
207205

208-
const INTAKE_PRESETS: Record<
209-
"incident" | "access" | "rollout" | "device",
210-
Partial<CaseIntake>
211-
> = {
212-
incident: {
213-
likely_category: "incident",
214-
urgency: "high",
215-
note_audience: "internal-note",
216-
},
217-
access: {
218-
likely_category: "access",
219-
urgency: "normal",
220-
note_audience: "internal-note",
221-
},
222-
rollout: {
223-
likely_category: "change-rollout",
224-
urgency: "normal",
225-
note_audience: "internal-note",
226-
},
227-
device: {
228-
likely_category: "device-environment",
229-
urgency: "normal",
230-
note_audience: "internal-note",
231-
},
232-
};
233-
234206
export const DraftTab = forwardRef<DraftTabHandle, DraftTabProps>(
235207
function DraftTab(
236208
{ initialDraft, onNavigateToSource, revampModeEnabled = false },
@@ -365,10 +337,6 @@ export const DraftTab = forwardRef<DraftTabHandle, DraftTabProps>(
365337
ConversationEntry[]
366338
>([]);
367339
const [handoffTouched, setHandoffTouched] = useState(false);
368-
const [caseIntake, setCaseIntake] = useState<CaseIntake>(() => ({
369-
...parseCaseIntake(null),
370-
note_audience: loadWorkspacePersonalization().preferred_note_audience,
371-
}));
372340
const [similarCases, setSimilarCases] = useState<SimilarCase[]>([]);
373341
const [similarCasesLoading, setSimilarCasesLoading] = useState(false);
374342
const [compareCase, setCompareCase] = useState<SimilarCase | null>(null);
@@ -471,52 +439,22 @@ export const DraftTab = forwardRef<DraftTabHandle, DraftTabProps>(
471439
void refreshWorkspaceCatalog();
472440
}, [refreshWorkspaceCatalog]);
473441

474-
const handleIntakeFieldChange = useCallback(
475-
(field: keyof CaseIntake, value: string) => {
476-
setCaseIntake((prev) => ({
477-
...prev,
478-
[field]: value,
479-
}));
480-
},
481-
[],
482-
);
483-
484-
const handleAnalyzeIntake = useCallback(() => {
485-
setCaseIntake((prev) =>
486-
analyzeCaseIntake(input, currentTicket ?? undefined, prev),
487-
);
488-
void logEvent("workspace_intake_analyzed", {
489-
ticket_id: currentTicketId,
490-
has_ticket: Boolean(currentTicketId),
491-
has_response: Boolean(response.trim()),
492-
});
493-
}, [input, currentTicket, logEvent, currentTicketId, response]);
494-
495-
const handleApplyIntakePreset = useCallback(
496-
(preset: "incident" | "access" | "rollout" | "device") => {
497-
setCaseIntake((prev) => ({
498-
...prev,
499-
...INTAKE_PRESETS[preset],
500-
}));
501-
void logEvent("workspace_intake_preset_applied", { preset });
502-
},
503-
[logEvent],
504-
);
505-
506-
const handleNoteAudienceChange = useCallback(
507-
(audience: NoteAudience) => {
508-
setCaseIntake((prev) => ({
509-
...prev,
510-
note_audience: audience,
511-
}));
512-
setWorkspacePersonalization((prev) => ({
513-
...prev,
514-
preferred_note_audience: audience,
515-
}));
516-
void logEvent("workspace_note_audience_changed", { audience });
517-
},
518-
[logEvent],
519-
);
442+
const {
443+
caseIntake,
444+
setCaseIntake,
445+
handleIntakeFieldChange,
446+
handleAnalyzeIntake,
447+
handleApplyIntakePreset,
448+
handleNoteAudienceChange,
449+
} = useDraftIntake({
450+
initialNoteAudience: workspacePersonalization.preferred_note_audience,
451+
input,
452+
currentTicket,
453+
currentTicketId,
454+
response,
455+
logEvent,
456+
setWorkspacePersonalization,
457+
});
520458

521459
const handleResponseLengthChange = useCallback((length: ResponseLength) => {
522460
setResponseLength(length);
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
// @vitest-environment jsdom
2+
import { act, renderHook } from "@testing-library/react";
3+
import { describe, expect, it, vi } from "vitest";
4+
import { INTAKE_PRESETS, useDraftIntake } from "./useDraftIntake";
5+
6+
function makeOptions(
7+
overrides: Partial<Parameters<typeof useDraftIntake>[0]> = {},
8+
) {
9+
return {
10+
initialNoteAudience: "internal-note" as const,
11+
input: "",
12+
currentTicket: null,
13+
currentTicketId: null,
14+
response: "",
15+
logEvent: vi.fn().mockResolvedValue(undefined),
16+
setWorkspacePersonalization: vi.fn(),
17+
...overrides,
18+
};
19+
}
20+
21+
describe("useDraftIntake", () => {
22+
it("updates individual intake fields", () => {
23+
const { result } = renderHook(() => useDraftIntake(makeOptions()));
24+
25+
act(() => {
26+
result.current.handleIntakeFieldChange("issue", "VPN outage");
27+
});
28+
expect(result.current.caseIntake.issue).toBe("VPN outage");
29+
30+
act(() => {
31+
result.current.handleIntakeFieldChange("impact", "west region");
32+
});
33+
expect(result.current.caseIntake.impact).toBe("west region");
34+
expect(result.current.caseIntake.issue).toBe("VPN outage");
35+
});
36+
37+
it("applies preset values and logs the event", () => {
38+
const logEvent = vi.fn().mockResolvedValue(undefined);
39+
const { result } = renderHook(() =>
40+
useDraftIntake(makeOptions({ logEvent })),
41+
);
42+
43+
act(() => {
44+
result.current.handleApplyIntakePreset("incident");
45+
});
46+
47+
expect(result.current.caseIntake.likely_category).toBe(
48+
INTAKE_PRESETS.incident.likely_category,
49+
);
50+
expect(result.current.caseIntake.urgency).toBe(
51+
INTAKE_PRESETS.incident.urgency,
52+
);
53+
expect(logEvent).toHaveBeenCalledWith("workspace_intake_preset_applied", {
54+
preset: "incident",
55+
});
56+
});
57+
58+
it("updates audience and syncs workspace personalization", () => {
59+
const setWorkspacePersonalization = vi.fn();
60+
const { result } = renderHook(() =>
61+
useDraftIntake(makeOptions({ setWorkspacePersonalization })),
62+
);
63+
64+
act(() => {
65+
result.current.handleNoteAudienceChange("customer-safe");
66+
});
67+
68+
expect(result.current.caseIntake.note_audience).toBe("customer-safe");
69+
expect(setWorkspacePersonalization).toHaveBeenCalled();
70+
});
71+
});
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import { useCallback, useState } from "react";
2+
import type { JiraTicket } from "../../hooks/useJira";
3+
import type {
4+
CaseIntake,
5+
NoteAudience,
6+
WorkspacePersonalization,
7+
} from "../../types/workspace";
8+
import {
9+
analyzeCaseIntake,
10+
parseCaseIntake,
11+
} from "../../features/workspace/workspaceAssistant";
12+
13+
export type IntakePreset = "incident" | "access" | "rollout" | "device";
14+
15+
export const INTAKE_PRESETS: Record<IntakePreset, Partial<CaseIntake>> = {
16+
incident: {
17+
likely_category: "incident",
18+
urgency: "high",
19+
note_audience: "internal-note",
20+
},
21+
access: {
22+
likely_category: "access",
23+
urgency: "normal",
24+
note_audience: "internal-note",
25+
},
26+
rollout: {
27+
likely_category: "change-rollout",
28+
urgency: "normal",
29+
note_audience: "internal-note",
30+
},
31+
device: {
32+
likely_category: "device-environment",
33+
urgency: "normal",
34+
note_audience: "internal-note",
35+
},
36+
};
37+
38+
interface UseDraftIntakeOptions {
39+
initialNoteAudience: NoteAudience;
40+
input: string;
41+
currentTicket: JiraTicket | null;
42+
currentTicketId: string | null;
43+
response: string;
44+
logEvent: (event: string, payload?: Record<string, unknown>) => Promise<void>;
45+
setWorkspacePersonalization: (
46+
updater: (prev: WorkspacePersonalization) => WorkspacePersonalization,
47+
) => void;
48+
}
49+
50+
export function useDraftIntake({
51+
initialNoteAudience,
52+
input,
53+
currentTicket,
54+
currentTicketId,
55+
response,
56+
logEvent,
57+
setWorkspacePersonalization,
58+
}: UseDraftIntakeOptions) {
59+
const [caseIntake, setCaseIntake] = useState<CaseIntake>(() => ({
60+
...parseCaseIntake(null),
61+
note_audience: initialNoteAudience,
62+
}));
63+
64+
const handleIntakeFieldChange = useCallback(
65+
(field: keyof CaseIntake, value: string) => {
66+
setCaseIntake((prev) => ({
67+
...prev,
68+
[field]: value,
69+
}));
70+
},
71+
[],
72+
);
73+
74+
const handleAnalyzeIntake = useCallback(() => {
75+
setCaseIntake((prev) =>
76+
analyzeCaseIntake(input, currentTicket ?? undefined, prev),
77+
);
78+
void logEvent("workspace_intake_analyzed", {
79+
ticket_id: currentTicketId,
80+
has_ticket: Boolean(currentTicketId),
81+
has_response: Boolean(response.trim()),
82+
});
83+
}, [input, currentTicket, logEvent, currentTicketId, response]);
84+
85+
const handleApplyIntakePreset = useCallback(
86+
(preset: IntakePreset) => {
87+
setCaseIntake((prev) => ({
88+
...prev,
89+
...INTAKE_PRESETS[preset],
90+
}));
91+
void logEvent("workspace_intake_preset_applied", { preset });
92+
},
93+
[logEvent],
94+
);
95+
96+
const handleNoteAudienceChange = useCallback(
97+
(audience: NoteAudience) => {
98+
setCaseIntake((prev) => ({
99+
...prev,
100+
note_audience: audience,
101+
}));
102+
setWorkspacePersonalization((prev) => ({
103+
...prev,
104+
preferred_note_audience: audience,
105+
}));
106+
void logEvent("workspace_note_audience_changed", { audience });
107+
},
108+
[logEvent, setWorkspacePersonalization],
109+
);
110+
111+
return {
112+
caseIntake,
113+
setCaseIntake,
114+
handleIntakeFieldChange,
115+
handleAnalyzeIntake,
116+
handleApplyIntakePreset,
117+
handleNoteAudienceChange,
118+
};
119+
}

0 commit comments

Comments
 (0)