Skip to content

Commit 7e47e4d

Browse files
authored
Merge pull request #54 from saagpatel/codex/refactor/wave5-7-draft-tab
refactor(components): extract 4 DraftTab hooks (Wave 5.7 Phase A part 1)
2 parents e5fe843 + a7e90d8 commit 7e47e4d

9 files changed

Lines changed: 947 additions & 383 deletions

src/components/Draft/DraftTab.tsx

Lines changed: 93 additions & 383 deletions
Large diffs are not rendered by default.
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
// @vitest-environment jsdom
2+
import { act, renderHook } from "@testing-library/react";
3+
import { describe, expect, it, vi } from "vitest";
4+
import { useDraftApproval } from "./useDraftApproval";
5+
6+
function makeOptions(
7+
overrides: Partial<Parameters<typeof useDraftApproval>[0]> = {},
8+
) {
9+
return {
10+
searchKb: vi.fn().mockResolvedValue([]),
11+
generateWithContextParams: vi
12+
.fn()
13+
.mockResolvedValue({ text: "summary", sources: [] }),
14+
modelLoaded: true,
15+
onShowError: vi.fn(),
16+
...overrides,
17+
};
18+
}
19+
20+
describe("useDraftApproval", () => {
21+
it("sets an error when searching with an empty query", async () => {
22+
const options = makeOptions();
23+
const { result } = renderHook(() => useDraftApproval(options));
24+
25+
await act(async () => {
26+
await result.current.handleApprovalSearch();
27+
});
28+
29+
expect(result.current.approvalError).toMatch(/enter a search term/i);
30+
expect(options.searchKb).not.toHaveBeenCalled();
31+
});
32+
33+
it("stores results from a successful approval search", async () => {
34+
const searchKb = vi
35+
.fn()
36+
.mockResolvedValue([
37+
{ id: "kb-1", title: "Approval Policy", snippet: "..." },
38+
]);
39+
const options = makeOptions({ searchKb });
40+
const { result } = renderHook(() => useDraftApproval(options));
41+
42+
act(() => {
43+
result.current.setApprovalQuery("password reset");
44+
});
45+
46+
await act(async () => {
47+
await result.current.handleApprovalSearch();
48+
});
49+
50+
expect(searchKb).toHaveBeenCalledWith("password reset", 5);
51+
expect(result.current.approvalResults).toHaveLength(1);
52+
expect(result.current.approvalError).toBeNull();
53+
});
54+
55+
it("blocks summarize when no model is loaded and surfaces a toast error", async () => {
56+
const onShowError = vi.fn();
57+
const options = makeOptions({ modelLoaded: false, onShowError });
58+
const { result } = renderHook(() => useDraftApproval(options));
59+
60+
act(() => {
61+
result.current.setApprovalQuery("vpn access");
62+
});
63+
64+
await act(async () => {
65+
await result.current.handleApprovalSummarize();
66+
});
67+
68+
expect(onShowError).toHaveBeenCalledWith(
69+
expect.stringContaining("No model loaded"),
70+
);
71+
expect(options.generateWithContextParams).not.toHaveBeenCalled();
72+
});
73+
});
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import { useCallback, useEffect, useState } from "react";
2+
import type { ContextSource, SearchResult } from "../../types/knowledge";
3+
4+
interface UseDraftApprovalOptions {
5+
searchKb: (query: string, limit: number) => Promise<SearchResult[]>;
6+
generateWithContextParams: (params: {
7+
user_input: string;
8+
kb_limit: number;
9+
response_length: "Short" | "Medium" | "Long";
10+
}) => Promise<{ text: string; sources: ContextSource[] }>;
11+
modelLoaded: boolean;
12+
onShowError: (message: string) => void;
13+
}
14+
15+
export function useDraftApproval({
16+
searchKb,
17+
generateWithContextParams,
18+
modelLoaded,
19+
onShowError,
20+
}: UseDraftApprovalOptions) {
21+
const [approvalQuery, setApprovalQuery] = useState("");
22+
const [approvalResults, setApprovalResults] = useState<SearchResult[]>([]);
23+
const [approvalSearching, setApprovalSearching] = useState(false);
24+
const [approvalSummary, setApprovalSummary] = useState("");
25+
const [approvalSummarizing, setApprovalSummarizing] = useState(false);
26+
const [approvalSources, setApprovalSources] = useState<ContextSource[]>([]);
27+
const [approvalError, setApprovalError] = useState<string | null>(null);
28+
29+
useEffect(() => {
30+
if (!approvalQuery.trim()) {
31+
setApprovalResults([]);
32+
setApprovalSummary("");
33+
setApprovalSources([]);
34+
setApprovalError(null);
35+
}
36+
}, [approvalQuery]);
37+
38+
const handleApprovalSearch = useCallback(async () => {
39+
if (!approvalQuery.trim()) {
40+
setApprovalError("Enter a search term to look up approvals.");
41+
return;
42+
}
43+
44+
setApprovalSearching(true);
45+
setApprovalError(null);
46+
try {
47+
const results = await searchKb(approvalQuery.trim(), 5);
48+
setApprovalResults(results);
49+
} catch (e) {
50+
console.error("Approval search failed:", e);
51+
setApprovalError("Approval search failed.");
52+
} finally {
53+
setApprovalSearching(false);
54+
}
55+
}, [approvalQuery, searchKb]);
56+
57+
const handleApprovalSummarize = useCallback(async () => {
58+
if (!approvalQuery.trim()) {
59+
setApprovalError("Enter a search term to summarize approvals.");
60+
return;
61+
}
62+
63+
if (!modelLoaded) {
64+
onShowError("No model loaded. Go to Settings to load a model.");
65+
return;
66+
}
67+
68+
setApprovalSummarizing(true);
69+
setApprovalError(null);
70+
try {
71+
const prompt = `Summarize the approval steps and owner(s) for: ${approvalQuery.trim()}. Keep it concise. If sources do not mention it, say so.`;
72+
const result = await generateWithContextParams({
73+
user_input: prompt,
74+
kb_limit: 5,
75+
response_length: "Short",
76+
});
77+
78+
setApprovalSummary(result.text);
79+
setApprovalSources(result.sources);
80+
} catch (e) {
81+
console.error("Approval summary failed:", e);
82+
setApprovalError("Approval summary failed.");
83+
} finally {
84+
setApprovalSummarizing(false);
85+
}
86+
}, [approvalQuery, modelLoaded, generateWithContextParams, onShowError]);
87+
88+
const resetApproval = useCallback(() => {
89+
setApprovalQuery("");
90+
setApprovalResults([]);
91+
setApprovalSummary("");
92+
setApprovalSources([]);
93+
setApprovalError(null);
94+
setApprovalSearching(false);
95+
setApprovalSummarizing(false);
96+
}, []);
97+
98+
return {
99+
approvalQuery,
100+
setApprovalQuery,
101+
approvalResults,
102+
setApprovalResults,
103+
approvalSearching,
104+
approvalSummary,
105+
setApprovalSummary,
106+
approvalSummarizing,
107+
approvalSources,
108+
setApprovalSources,
109+
approvalError,
110+
setApprovalError,
111+
handleApprovalSearch,
112+
handleApprovalSummarize,
113+
resetApproval,
114+
};
115+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
// @vitest-environment jsdom
2+
import { act, renderHook } from "@testing-library/react";
3+
import { describe, expect, it, vi } from "vitest";
4+
import { useDraftChecklist } from "./useDraftChecklist";
5+
6+
function makeOptions(
7+
overrides: Partial<Parameters<typeof useDraftChecklist>[0]> = {},
8+
) {
9+
return {
10+
input: "user vpn disconnects",
11+
ocrText: null,
12+
diagnosticNotes: "",
13+
treeResult: null,
14+
currentTicket: null,
15+
modelLoaded: true,
16+
generateChecklist: vi
17+
.fn()
18+
.mockResolvedValue({ items: [{ id: "a", label: "Ping gateway" }] }),
19+
updateChecklist: vi
20+
.fn()
21+
.mockResolvedValue({ items: [{ id: "a", label: "Ping gateway" }] }),
22+
onShowError: vi.fn(),
23+
...overrides,
24+
};
25+
}
26+
27+
describe("useDraftChecklist", () => {
28+
it("generates a checklist and stores items", async () => {
29+
const options = makeOptions();
30+
const { result } = renderHook(() => useDraftChecklist(options));
31+
32+
await act(async () => {
33+
await result.current.handleChecklistGenerate();
34+
});
35+
36+
expect(options.generateChecklist).toHaveBeenCalledWith(
37+
expect.objectContaining({ user_input: "user vpn disconnects" }),
38+
);
39+
expect(result.current.checklistItems).toHaveLength(1);
40+
expect(result.current.checklistError).toBeNull();
41+
});
42+
43+
it("sets a local error when prompt input is empty", async () => {
44+
const options = makeOptions({
45+
input: "",
46+
ocrText: null,
47+
currentTicket: null,
48+
});
49+
const { result } = renderHook(() => useDraftChecklist(options));
50+
51+
await act(async () => {
52+
await result.current.handleChecklistGenerate();
53+
});
54+
55+
expect(result.current.checklistError).toMatch(
56+
/add ticket details or notes/i,
57+
);
58+
expect(options.generateChecklist).not.toHaveBeenCalled();
59+
});
60+
61+
it("toggles completion state per id", () => {
62+
const { result } = renderHook(() => useDraftChecklist(makeOptions()));
63+
64+
act(() => {
65+
result.current.handleChecklistToggle("a");
66+
});
67+
expect(result.current.checklistCompleted).toEqual({ a: true });
68+
69+
act(() => {
70+
result.current.handleChecklistToggle("a");
71+
});
72+
expect(result.current.checklistCompleted).toEqual({ a: false });
73+
});
74+
});

0 commit comments

Comments
 (0)