Skip to content

Commit dcf218a

Browse files
saagar210claude
andcommitted
refactor(components): extract handleConversationSubmit to new hook
Conversation-mode submit handler in DraftTab was a 52-line useCallback that appended an input entry, kicked off a streaming generation, then appended a response entry with metrics. Its dep array was also incomplete — listing only [modelLoaded, responseLength, generateStreaming, clearStreamingText] while the body closed over ~9 additional state setters, which was the source of one of the existing react-hooks/exhaustive-deps warnings in the file. Extracts to useConversationSubmit (new sibling hook) with: - Explicit options interface: 4 reads + 9 writers grouped at the call site for readability - Complete dep array with every closed-over reference - Local GenerateStreamingFn type alias that mirrors useLlm's signature (query, responseLength?, options?) so the hook doesn't have to import from the useLlm call chain DraftTab's call site drops to 14 lines of named bindings. The unused value import of ConversationEntry in DraftTab becomes a pure type import, matching the Wave 5 pattern for sibling-hook-only types. DraftTab.tsx: 1207 -> 1171 lines (-36). Branch total vs master: 1412 -> 1171 (-241, -17.1%). ESLint warnings in DraftTab: 4 -> 2 across the whole branch (one from useNextActionHandler last commit, one from this one). No behavior change. Typecheck + vitest (241/241) + eslint clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent ce0555a commit dcf218a

2 files changed

Lines changed: 144 additions & 54 deletions

File tree

src/components/Draft/DraftTab.tsx

Lines changed: 18 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ import { auditResponseCopyOverride, exportDraft } from "./draftTauriCommands";
1919
import { DraftResponsePanel } from "./DraftResponsePanel";
2020
import { InputPanel } from "./InputPanel";
2121
import { DiagnosisPanel, TreeResult } from "./DiagnosisPanel";
22-
import { ConversationThread, ConversationEntry } from "./ConversationThread";
22+
import { ConversationThread } from "./ConversationThread";
23+
import type { ConversationEntry } from "./ConversationThread";
2324
import { useDraftApproval } from "./useDraftApproval";
2425
import { useDraftChecklist } from "./useDraftChecklist";
2526
import { useDraftFirstResponse } from "./useDraftFirstResponse";
@@ -33,6 +34,7 @@ import { useResponseActions } from "./useResponseActions";
3334
import { useWorkspaceArtifacts } from "./useWorkspaceArtifacts";
3435
import { useWorkspaceClipboardPacks } from "./useWorkspaceClipboardPacks";
3536
import { ConversationInput } from "./ConversationInput";
37+
import { useConversationSubmit } from "./useConversationSubmit";
3638
import { WorkspaceDialogs } from "./WorkspaceDialogs";
3739
import { WorkspaceModeShell } from "./WorkspaceModeShell";
3840
import { WorkspacePanels } from "./WorkspacePanels";
@@ -518,59 +520,21 @@ export const DraftTab = forwardRef<DraftTabHandle, DraftTabProps>(
518520
[],
519521
);
520522

521-
const handleConversationSubmit = useCallback(
522-
async (text: string) => {
523-
if (!modelLoaded) return;
524-
525-
// Add input entry
526-
const inputEntry: ConversationEntry = {
527-
id: crypto.randomUUID(),
528-
type: "input",
529-
timestamp: new Date().toISOString(),
530-
content: text,
531-
};
532-
setConversationEntries((prev) => [...prev, inputEntry]);
533-
setInput(text);
534-
535-
// Generate
536-
setGenerating(true);
537-
setResponse("");
538-
clearStreamingText();
539-
setConfidence(null);
540-
setGrounding([]);
541-
try {
542-
const result = await generateStreaming(text, responseLength, {});
543-
setResponse(result.text);
544-
setOriginalResponse(result.text);
545-
setIsResponseEdited(false);
546-
setSources(result.sources);
547-
setConfidence(result.confidence ?? null);
548-
setGrounding(result.grounding ?? []);
549-
550-
// Add response entry
551-
const responseEntry: ConversationEntry = {
552-
id: crypto.randomUUID(),
553-
type: "response",
554-
timestamp: new Date().toISOString(),
555-
content: result.text,
556-
sources: result.sources,
557-
metrics: result.metrics
558-
? {
559-
tokens_per_second: result.metrics.tokens_per_second,
560-
sources_used: result.metrics.sources_used,
561-
word_count: result.metrics.word_count,
562-
}
563-
: undefined,
564-
};
565-
setConversationEntries((prev) => [...prev, responseEntry]);
566-
} catch (e) {
567-
console.error("Generation failed:", e);
568-
} finally {
569-
setGenerating(false);
570-
}
571-
},
572-
[modelLoaded, responseLength, generateStreaming, clearStreamingText],
573-
);
523+
const handleConversationSubmit = useConversationSubmit({
524+
modelLoaded,
525+
responseLength,
526+
generateStreaming,
527+
clearStreamingText,
528+
setConversationEntries,
529+
setInput,
530+
setGenerating,
531+
setResponse,
532+
setOriginalResponse,
533+
setIsResponseEdited,
534+
setSources,
535+
setConfidence,
536+
setGrounding,
537+
});
574538

575539
const buildDiagnosisJson = useCallback(
576540
() =>
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import { useCallback } from "react";
2+
import type { Dispatch, SetStateAction } from "react";
3+
import type { ConversationEntry } from "./ConversationThread";
4+
import type {
5+
ConfidenceAssessment,
6+
GenerateWithContextResult,
7+
GroundedClaim,
8+
JiraTicketContext,
9+
TreeDecisions,
10+
} from "../../types/llm";
11+
import type { ContextSource } from "../../types/knowledge";
12+
import type { ResponseLength } from "../../types/workspace";
13+
14+
export type GenerateStreamingFn = (
15+
query: string,
16+
responseLength?: ResponseLength,
17+
options?: {
18+
onToken?: (token: string) => void;
19+
treeDecisions?: TreeDecisions;
20+
diagnosticNotes?: string;
21+
jiraTicket?: JiraTicketContext;
22+
},
23+
) => Promise<GenerateWithContextResult>;
24+
25+
export interface UseConversationSubmitOptions {
26+
modelLoaded: boolean;
27+
responseLength: ResponseLength;
28+
generateStreaming: GenerateStreamingFn;
29+
clearStreamingText: () => void;
30+
setConversationEntries: Dispatch<SetStateAction<ConversationEntry[]>>;
31+
setInput: Dispatch<SetStateAction<string>>;
32+
setGenerating: (value: boolean) => void;
33+
setResponse: Dispatch<SetStateAction<string>>;
34+
setOriginalResponse: Dispatch<SetStateAction<string>>;
35+
setIsResponseEdited: Dispatch<SetStateAction<boolean>>;
36+
setSources: Dispatch<SetStateAction<ContextSource[]>>;
37+
setConfidence: Dispatch<SetStateAction<ConfidenceAssessment | null>>;
38+
setGrounding: Dispatch<SetStateAction<GroundedClaim[]>>;
39+
}
40+
41+
/**
42+
* Handles submission of a conversation-mode message: appends the input entry
43+
* to the transcript, streams a generation, then appends the response entry
44+
* with its metrics. Extracted from DraftTab so the 50-line handler lives
45+
* alongside the other Draft hooks.
46+
*/
47+
export function useConversationSubmit({
48+
modelLoaded,
49+
responseLength,
50+
generateStreaming,
51+
clearStreamingText,
52+
setConversationEntries,
53+
setInput,
54+
setGenerating,
55+
setResponse,
56+
setOriginalResponse,
57+
setIsResponseEdited,
58+
setSources,
59+
setConfidence,
60+
setGrounding,
61+
}: UseConversationSubmitOptions) {
62+
return useCallback(
63+
async (text: string) => {
64+
if (!modelLoaded) return;
65+
66+
const inputEntry: ConversationEntry = {
67+
id: crypto.randomUUID(),
68+
type: "input",
69+
timestamp: new Date().toISOString(),
70+
content: text,
71+
};
72+
setConversationEntries((prev) => [...prev, inputEntry]);
73+
setInput(text);
74+
75+
setGenerating(true);
76+
setResponse("");
77+
clearStreamingText();
78+
setConfidence(null);
79+
setGrounding([]);
80+
try {
81+
const result = await generateStreaming(text, responseLength, {});
82+
setResponse(result.text);
83+
setOriginalResponse(result.text);
84+
setIsResponseEdited(false);
85+
setSources(result.sources);
86+
setConfidence(result.confidence ?? null);
87+
setGrounding(result.grounding ?? []);
88+
89+
const responseEntry: ConversationEntry = {
90+
id: crypto.randomUUID(),
91+
type: "response",
92+
timestamp: new Date().toISOString(),
93+
content: result.text,
94+
sources: result.sources,
95+
metrics: result.metrics
96+
? {
97+
tokens_per_second: result.metrics.tokens_per_second,
98+
sources_used: result.metrics.sources_used,
99+
word_count: result.metrics.word_count,
100+
}
101+
: undefined,
102+
};
103+
setConversationEntries((prev) => [...prev, responseEntry]);
104+
} catch (e) {
105+
console.error("Generation failed:", e);
106+
} finally {
107+
setGenerating(false);
108+
}
109+
},
110+
[
111+
modelLoaded,
112+
responseLength,
113+
generateStreaming,
114+
clearStreamingText,
115+
setConversationEntries,
116+
setInput,
117+
setGenerating,
118+
setResponse,
119+
setOriginalResponse,
120+
setIsResponseEdited,
121+
setSources,
122+
setConfidence,
123+
setGrounding,
124+
],
125+
);
126+
}

0 commit comments

Comments
 (0)