diff --git a/src/components/Draft/DraftTab.tsx b/src/components/Draft/DraftTab.tsx index a3dd6e1..43e3923 100644 --- a/src/components/Draft/DraftTab.tsx +++ b/src/components/Draft/DraftTab.tsx @@ -16,9 +16,7 @@ import { readPanelDensityMode, } from "./draftTabDefaults"; import { auditResponseCopyOverride, exportDraft } from "./draftTauriCommands"; -import { DraftResponsePanel } from "./DraftResponsePanel"; -import { InputPanel } from "./InputPanel"; -import { DiagnosisPanel, TreeResult } from "./DiagnosisPanel"; +import type { TreeResult } from "./DiagnosisPanel"; import { ConversationThread } from "./ConversationThread"; import type { ConversationEntry } from "./ConversationThread"; import { useDraftApproval } from "./useDraftApproval"; @@ -30,7 +28,6 @@ import { useDraftIntake } from "./useDraftIntake"; import { useDraftLifecycle } from "./useDraftLifecycle"; import { useDraftPersistence } from "./useDraftPersistence"; import { useGuidedRunbook } from "./useGuidedRunbook"; -import { useNextActionHandler } from "./useNextActionHandler"; import { useResponseActions } from "./useResponseActions"; import { useWorkspaceArtifacts } from "./useWorkspaceArtifacts"; import { useWorkspaceClipboardPacks } from "./useWorkspaceClipboardPacks"; @@ -38,8 +35,6 @@ import { ConversationInput } from "./ConversationInput"; import { useConversationSubmit } from "./useConversationSubmit"; import { WorkspaceDialogs } from "./WorkspaceDialogs"; import { WorkspaceModeShell } from "./WorkspaceModeShell"; -import { WorkspacePanels } from "./WorkspacePanels"; -import { WorkspaceWorkflowStrip } from "./WorkspaceWorkflowStrip"; import { useLlmGeneration } from "../../hooks/useLlmGeneration"; import { useLlmStreaming } from "../../hooks/useLlmStreaming"; import { useDrafts } from "../../hooks/useDrafts"; @@ -53,12 +48,11 @@ import { useToastContext } from "../../contexts/ToastContext"; import { useAppStatus } from "../../contexts/AppStatusContext"; import { AiReadinessBanner } from "./AiReadinessBanner"; import { resolveRevampFlags } from "../../features/revamp"; -import { TicketWorkspaceRail } from "../../features/workspace/TicketWorkspaceRail"; +import { ClaudeDesignWorkspace } from "../../features/workspace/ClaudeDesignWorkspace"; import { useWorkspaceCatalog } from "../../features/workspace/useWorkspaceCatalog"; import { useWorkspaceDerivedArtifacts } from "../../features/workspace/useWorkspaceDerivedArtifacts"; import { useWorkspaceCommandBridge } from "../../features/workspace/useWorkspaceCommandBridge"; import { useWorkspaceDraftState } from "../../features/workspace/useWorkspaceDraftState"; -import { countWords } from "../../features/analytics/qualityMetrics"; import type { JiraTicket } from "../../hooks/useJira"; import type { ConfidenceAssessment, @@ -114,7 +108,6 @@ export const DraftTab = forwardRef( updateDraft, triggerAutosave, cancelAutosave, - templates, loadTemplates, searchDrafts, getDraft, @@ -157,18 +150,12 @@ export const DraftTab = forwardRef( const { approvalQuery, setApprovalQuery, - approvalResults, setApprovalResults, - approvalSearching, approvalSummary, setApprovalSummary, - approvalSummarizing, approvalSources, setApprovalSources, - approvalError, setApprovalError, - handleApprovalSearch, - handleApprovalSummarize, resetApproval, } = useDraftApproval({ searchKb, @@ -193,7 +180,8 @@ export const DraftTab = forwardRef( const [responseLength, setResponseLength] = useState( () => loadWorkspacePersonalization().preferred_output_length, ); - const [diagnosisCollapsed, setDiagnosisCollapsed] = useState(false); + // Diagnosis is always expanded in the Claude Design workspace layout. + const diagnosisCollapsed = false; const [currentTicketId, setCurrentTicketId] = useState(null); const [currentTicket, setCurrentTicket] = useState(null); const [originalResponse, setOriginalResponse] = useState(""); @@ -226,17 +214,13 @@ export const DraftTab = forwardRef( saveAlternative, chooseAlternative, } = useAlternatives(); - const { suggestions, findSimilar, saveAsTemplate, incrementUsage } = - useSavedResponses(); - const [suggestionsDismissed, setSuggestionsDismissed] = useState(false); + const { findSimilar, saveAsTemplate, incrementUsage } = useSavedResponses(); + const [, setSuggestionsDismissed] = useState(false); const { generating, setGenerating, - generatingAlternative, handleGenerate, - handleGenerateAlternative, - handleChooseAlternative, handleUseAlternative, resetGeneration, } = useDraftGeneration({ @@ -272,10 +256,6 @@ export const DraftTab = forwardRef( setFirstResponse, firstResponseTone, setFirstResponseTone, - firstResponseGenerating, - handleGenerateFirstResponse, - handleCopyFirstResponse, - handleClearFirstResponse, resetFirstResponse, } = useDraftFirstResponse({ input, @@ -292,14 +272,7 @@ export const DraftTab = forwardRef( setChecklistItems, checklistCompleted, setChecklistCompleted, - checklistGenerating, - checklistUpdating, - checklistError, setChecklistError, - handleChecklistGenerate, - handleChecklistUpdate, - handleChecklistToggle, - handleChecklistClear, resetChecklist, } = useDraftChecklist({ input, @@ -314,12 +287,10 @@ export const DraftTab = forwardRef( }); const { - resolutionKits, workspaceFavorites, runbookTemplates, guidedRunbookSession, setGuidedRunbookSession, - workspaceCatalogLoading, runbookSessionSourceScopeKey, runbookSessionTouched, setRunbookSessionSourceScopeKey, @@ -345,8 +316,6 @@ export const DraftTab = forwardRef( setCaseIntake, handleIntakeFieldChange, handleAnalyzeIntake, - handleApplyIntakePreset, - handleNoteAudienceChange, } = useDraftIntake({ initialNoteAudience: workspacePersonalization.preferred_note_audience, input, @@ -357,14 +326,7 @@ export const DraftTab = forwardRef( setWorkspacePersonalization, }); - const { - guidedRunbookNote, - setGuidedRunbookNote, - handleStartGuidedRunbook, - handleAdvanceGuidedRunbook, - handleCopyRunbookProgressToNotes, - handleGuidedRunbookNoteChange, - } = useGuidedRunbook({ + const { guidedRunbookNote, setGuidedRunbookNote } = useGuidedRunbook({ runbookTemplates, guidedRunbookSession, workspaceRunbookScopeKey, @@ -390,36 +352,12 @@ export const DraftTab = forwardRef( })); }, []); - const handleWorkspacePersonalizationChange = useCallback( - (patch: Partial) => { - setWorkspacePersonalization((prev) => { - const next = { ...prev, ...patch }; - if (patch.preferred_note_audience && !savedDraftId) { - setCaseIntake((current) => ({ - ...current, - note_audience: - current.note_audience ?? - patch.preferred_note_audience ?? - next.preferred_note_audience, - })); - } - if (patch.preferred_output_length) { - setResponseLength(patch.preferred_output_length); - } - return next; - }); - }, - [savedDraftId, setCaseIntake], - ); - const { showTemplateModal, setShowTemplateModal, templateModalRating, - handleApplyTemplate, handleSaveAsTemplate, handleTemplateModalSave, - handleResponseChange, handleCancel, handleCopyResponse, handleExportResponse, @@ -446,28 +384,12 @@ export const DraftTab = forwardRef( onShowError: showError, }); - const handleSuggestionApply = useCallback( - (content: string, templateId: string) => { - setResponse(content); - setOriginalResponse(content); - setIsResponseEdited(false); - incrementUsage(templateId); - setSuggestionsDismissed(true); - }, - [incrementUsage], - ); - - const handleSuggestionDismiss = useCallback(() => { - setSuggestionsDismissed(true); - }, []); - - const handleTreeComplete = useCallback((result: TreeResult) => { - setTreeResult(result); - }, []); - - const handleTreeClear = useCallback(() => { - setTreeResult(null); - }, []); + // Note: legacy suggestion apply/dismiss and tree handlers were tied to + // InputPanel/DiagnosisPanel; the Claude Design workspace layout does not + // surface these controls (they were never promoted to PR-worthy UX). + // `incrementUsage` is still available via useSavedResponses above for + // future wiring. + void incrementUsage; const handleViewModeChange = useCallback( (mode: "panels" | "conversation") => { @@ -541,15 +463,10 @@ export const DraftTab = forwardRef( handoffPack, serializedCaseIntake, activeWorkspaceDraft, - missingQuestions, - nextActions, evidencePack, kbDraft, hasSaveableWorkspaceContent, hasLiveWorkspaceContent, - responseWordCount, - responseEditRatio, - checklistCompletedCount, } = useWorkspaceDerivedArtifacts({ structuredIntakeEnabled, nextBestActionEnabled, @@ -578,16 +495,9 @@ export const DraftTab = forwardRef( }); const { - similarCases, - similarCasesLoading, - compareCase, setCompareCase, handleRefreshSimilarCases, handleCompareLastResolution, - handleCompareSimilarCase, - handleSaveCurrentResolutionKit, - handleApplyResolutionKit, - handleToggleWorkspaceFavorite, resetWorkspaceArtifacts, } = useWorkspaceArtifacts({ similarCasesEnabled, @@ -745,54 +655,12 @@ export const DraftTab = forwardRef( [getDraft, handleLoadDraft], ); - const handleOpenSimilarCase = useCallback( - async (similarCase: SimilarCase) => { - if (!requestOpenSimilarCase(similarCase)) { - return; - } - - try { - await loadSimilarCaseIntoWorkspace(similarCase); - setPendingSimilarCaseOpen(null); - void logEvent("workspace_similar_case_opened", { - ticket_id: currentTicketId, - similar_case_id: similarCase.draft_id, - similar_case_ticket: similarCase.ticket_id, - }); - showSuccess("Loaded similar case into the workspace"); - } catch { - showError("Failed to open similar case"); - } - }, - [ - loadSimilarCaseIntoWorkspace, - logEvent, - currentTicketId, - requestOpenSimilarCase, - setPendingSimilarCaseOpen, - showError, - showSuccess, - ], - ); - - const handleAcceptNextAction = useNextActionHandler({ - currentTicketId, - currentTicketSummary: currentTicket?.summary, - diagnosticNotes, - input, - caseIntakeIssue: caseIntake.issue, - missingQuestions, - runbookTemplates, - setDiagnosticNotes, - setPanelDensityMode, - setApprovalQuery, - setCaseIntake, - handleGenerate, - handleStartGuidedRunbook, - handleCopyKbDraft, - logEvent, - onShowSuccess: showSuccess, - }); + // handleOpenSimilarCase + handleAcceptNextAction were consumed by the + // old TicketWorkspaceRail; the Claude Design layout does not surface + // those flows. Helpers kept available for future wiring. + void loadSimilarCaseIntoWorkspace; + void requestOpenSimilarCase; + void setPendingSimilarCaseOpen; useWorkspaceCommandBridge({ enabled: workspaceRailEnabled, @@ -924,189 +792,41 @@ export const DraftTab = forwardRef( /> ); - const workflowStrip = ( - { - void handleSaveDraft(); - }} - /> - ); - - const inputPanel = ( - - ); - - const diagnosisPanel = ( - setDiagnosisCollapsed(!diagnosisCollapsed)} - /> - ); - - const responsePanel = ( - - ); - - const workspacePanel = ( - setCompareCase(null), - }} - packs={{ - handoffPack, - evidencePack, - kbDraft, - onCopyHandoff: handleCopyHandoffPack, - onCopyEvidence: handleCopyEvidencePack, - onCopyKb: handleCopyKbDraft, - }} - kits={{ - items: resolutionKits, - onSaveCurrent: handleSaveCurrentResolutionKit, - onApply: handleApplyResolutionKit, - }} - favorites={{ - items: workspaceFavorites, - onToggle: handleToggleWorkspaceFavorite, - }} - runbooks={{ - templates: runbookTemplates, - session: guidedRunbookSession, - note: guidedRunbookNote, - onNoteChange: handleGuidedRunbookNoteChange, - onStart: handleStartGuidedRunbook, - onAdvance: handleAdvanceGuidedRunbook, - onCopyProgress: handleCopyRunbookProgressToNotes, - }} - personalization={{ - value: workspacePersonalization, - onChange: handleWorkspacePersonalizationChange, + generating={generating} + modelLoaded={modelLoaded} + loadedModelName={loadedModelName} + caseIntake={caseIntake} + onIntakeFieldChange={(field, value) => { + handleIntakeFieldChange(field, value ?? ""); }} - workspaceCatalogLoading={workspaceCatalogLoading} - currentResponse={response} + onGenerate={handleGenerate} + onCancel={handleCancel} + onCopyResponse={handleCopyResponse} + onSaveAsTemplate={() => handleSaveAsTemplate(0)} + onUseAlternative={(alt) => handleUseAlternative(alt.alternative_text)} + onNavigateToSource={onNavigateToSource} /> ); @@ -1154,17 +874,8 @@ export const DraftTab = forwardRef( onCancel={handleCancel} /> } - workflowStrip={workflowStrip} - panels={ - - } + workflowStrip={null} + panels={claudeDesignWorkspacePanel} dialogs={dialogs} /> ); diff --git a/src/features/workspace/ClaudeDesignWorkspace.tsx b/src/features/workspace/ClaudeDesignWorkspace.tsx new file mode 100644 index 0000000..77ea685 --- /dev/null +++ b/src/features/workspace/ClaudeDesignWorkspace.tsx @@ -0,0 +1,797 @@ +/** + * Claude Design Workspace — the Draft flow rendered in the layout of + * the Claude Design handoff bundle (assistsupport/project/AssistSupport Workspace.html). + * + * This component is a pure renderer: it consumes state + handlers from + * the existing DraftTab hooks and emits JSX/classnames matching the + * handoff's `.ws`, `.ticket`, `.ws-strip`, `.panel`, `.chip`, `.seg`, + * `.gauge`, `.intent`, `.sources`, `.alternatives` styles defined in + * `src/styles/revamp/claudeDesignWorkspace.css`. + */ + +import { useMemo } from "react"; +import type { ReactNode } from "react"; +import { Icon } from "../../components/shared/Icon"; +import type { + ConfidenceAssessment, + GenerationMetrics, + GroundedClaim, +} from "../../types/llm"; +import type { ContextSource } from "../../types/knowledge"; +import type { + CaseIntake, + IntakeUrgency, + NoteAudience, + ResponseAlternative, + ResponseLength, +} from "../../types/workspace"; +import type { JiraTicket } from "../../hooks/useJira"; + +export interface ClaudeDesignWorkspaceProps { + // Ticket + ticket: JiraTicket | null; + ticketId: string | null; + + // Input + input: string; + onInputChange: (value: string) => void; + responseLength: ResponseLength; + onResponseLengthChange: (length: ResponseLength) => void; + + // Workflow status + hasInput: boolean; + hasDiagnosis: boolean; + hasResponseReady: boolean; + handoffTouched: boolean; + + // Response + response: string; + streamingText: string; + isStreaming: boolean; + sources: ContextSource[]; + metrics: GenerationMetrics | null; + confidence: ConfidenceAssessment | null; + grounding: GroundedClaim[]; + alternatives: ResponseAlternative[]; + + // State + generating: boolean; + modelLoaded: boolean; + loadedModelName: string | null; + + // Intake + caseIntake: CaseIntake; + onIntakeFieldChange: ( + field: "note_audience" | "likely_category" | "urgency" | "environment", + value: string | null, + ) => void; + + // Actions + onGenerate: () => void; + onCancel: () => void; + onCopyResponse: () => void; + onSaveAsTemplate: () => void; + onUseAlternative: (alt: ResponseAlternative) => void; + onNavigateToSource?: (searchQuery: string) => void; +} + +const INTENT_CHIPS: ReadonlyArray<{ + value: string; + label: string; +}> = [ + { value: "policy", label: "Policy" }, + { value: "howto", label: "Howto" }, + { value: "access", label: "Access" }, + { value: "incident", label: "Incident" }, +]; + +const LENGTH_OPTIONS: ReadonlyArray = [ + "Short", + "Medium", + "Long", +]; + +const URGENCY_OPTIONS: ReadonlyArray = [ + "", + "low", + "normal", + "high", + "critical", +]; + +const AUDIENCE_OPTIONS: ReadonlyArray<{ value: NoteAudience; label: string }> = + [ + { value: "customer-safe", label: "End user (customer-safe)" }, + { value: "internal-note", label: "Internal note" }, + { value: "escalation-note", label: "Escalation note" }, + ]; + +const TONE_OPTIONS: ReadonlyArray<{ value: string; label: string }> = [ + { value: "neutral", label: "Neutral" }, + { value: "empathetic", label: "Empathetic" }, + { value: "direct", label: "Direct" }, +]; + +/** + * Derive initials for the ticket avatar — "Priya Anand" -> "PA". + */ +function initialsFor(name: string | null | undefined): string { + if (!name) return "?"; + const parts = name.trim().split(/\s+/).slice(0, 2); + return parts.map((p) => p[0]?.toUpperCase() ?? "").join("") || "?"; +} + +/** + * Format a timestamp like "2h ago" from an ISO string. + * Falls back to the raw string if parsing fails. + */ +function timeAgo(iso: string | null | undefined): string { + if (!iso) return ""; + const t = Date.parse(iso); + if (Number.isNaN(t)) return iso; + const seconds = Math.max(0, Math.floor((Date.now() - t) / 1000)); + if (seconds < 60) return `${seconds}s ago`; + if (seconds < 3600) return `${Math.floor(seconds / 60)}m ago`; + if (seconds < 86400) return `${Math.floor(seconds / 3600)}h ago`; + return `${Math.floor(seconds / 86400)}d ago`; +} + +/** + * Derive a lowercase, underscored intent class for the banner from + * the free-text likely_category that the intake analyzer produced. + * e.g. "Policy / removable media" -> "policy_removable_media". + */ +function deriveIntentClass(category: string | null | undefined): string | null { + if (!category) return null; + const slug = category + .toLowerCase() + .replace(/[^a-z0-9]+/g, "_") + .replace(/^_+|_+$/g, ""); + return slug || null; +} + +/** + * Render response text with inline [n] citations as accent cite pills. + * Falls back to plain text when no citation markers are found. + */ +function renderResponseWithCitations( + text: string, + sources: ContextSource[], + onNavigateToSource: ((searchQuery: string) => void) | undefined, +): ReactNode[] { + if (!text) return []; + const parts: ReactNode[] = []; + const re = /\[(\d+)\]/g; + let lastIndex = 0; + let match: RegExpExecArray | null; + let keyIdx = 0; + while ((match = re.exec(text)) !== null) { + if (match.index > lastIndex) { + parts.push(text.slice(lastIndex, match.index)); + } + const n = Number.parseInt(match[1] ?? "0", 10); + const source = n > 0 ? sources[n - 1] : undefined; + const title = source?.title ?? source?.file_path ?? `Source ${n}`; + const searchQuery = source?.title ?? source?.file_path ?? ""; + parts.push( + , + ); + lastIndex = re.lastIndex; + } + if (lastIndex < text.length) { + parts.push(text.slice(lastIndex)); + } + return parts; +} + +function formatPercent(value: number | null | undefined): string { + if (value == null || Number.isNaN(value)) return "--"; + return `${Math.round(value * 100)}%`; +} + +export function ClaudeDesignWorkspace({ + ticket, + ticketId, + input, + onInputChange, + responseLength, + onResponseLengthChange, + hasInput, + hasDiagnosis, + hasResponseReady, + handoffTouched, + response, + streamingText, + isStreaming, + sources, + metrics, + confidence, + grounding, + alternatives, + generating, + modelLoaded, + loadedModelName, + caseIntake, + onIntakeFieldChange, + onGenerate, + onCancel, + onCopyResponse, + onSaveAsTemplate, + onUseAlternative, + onNavigateToSource, +}: ClaudeDesignWorkspaceProps) { + const displayedResponse = isStreaming ? streamingText : response; + const hasResponse = Boolean(displayedResponse.trim()); + + const activeIntent = useMemo( + () => deriveIntentClass(caseIntake.likely_category), + [caseIntake.likely_category], + ); + + const stages = useMemo(() => { + // 4-step workflow: Triage -> Classify -> Draft -> Send + const triageDone = hasInput; + const classifyDone = hasDiagnosis || Boolean(activeIntent); + const draftDone = hasResponseReady; + const sendDone = handoffTouched; + let activeIdx = 0; + if (!triageDone) activeIdx = 0; + else if (!classifyDone) activeIdx = 1; + else if (!draftDone) activeIdx = 2; + else activeIdx = 3; + return [ + { n: 1, label: "Triage", done: triageDone }, + { n: 2, label: "Classify", done: classifyDone }, + { n: 3, label: "Draft response", done: draftDone }, + { n: 4, label: "Send to Jira", done: sendDone }, + ].map((s, i) => ({ + ...s, + state: s.done ? "done" : i === activeIdx ? "active" : "", + })); + }, [hasInput, hasDiagnosis, activeIntent, hasResponseReady, handoffTouched]); + + const gaugePercent = confidence ? Math.round(confidence.score * 100) : null; + const gaugeWidth = gaugePercent != null ? `${gaugePercent}%` : "0%"; + const confidenceTone = !confidence + ? null + : confidence.mode === "answer" + ? "good" + : confidence.mode === "clarify" + ? "warn" + : "bad"; + + const groundedClaimCount = grounding.length; + const supportedClaimCount = grounding.filter( + (g: GroundedClaim) => g.support_level === "supported", + ).length; + + const sourcesCount = sources.length; + const wordCount = metrics?.word_count ?? 0; + const tokensPerSec = metrics?.tokens_per_second ?? 0; + const contextUtilPct = + metrics?.context_utilization != null + ? Math.round(metrics.context_utilization * 100) + : null; + + const ticketPriority = ticket?.priority ?? "Normal"; + const ticketReporter = ticket?.reporter ?? "—"; + const ticketSummary = ticket?.summary ?? "No ticket loaded"; + const ticketKey = ticket?.key ?? ticketId; + const ticketOpened = ticket?.created ? timeAgo(ticket.created) : ""; + const ticketIssueType = ticket?.issue_type ?? "Request"; + + const intakeCategoryChip = (caseIntake.likely_category ?? "").toLowerCase(); + + return ( +
+ {/* ===== TICKET HEADER CARD ===== */} +
+
{initialsFor(ticketReporter)}
+
+
+ {ticketKey + ? `${ticketKey} · ${ticketIssueType.toUpperCase()}` + : "NO TICKET LOADED"} +
+
{ticketSummary}
+
+ + {ticketReporter} + + {ticket?.status && ( + <> + · + {ticket.status} + + )} + {ticket?.assignee && ( + <> + · + → {ticket.assignee} + + )} + {ticketOpened && ( + <> + · + + {ticketOpened} + + + )} +
+
+
+ {ticketPriority} + {caseIntake.likely_category && ( + + {caseIntake.likely_category} + + )} +
+
+ + {/* ===== WORKFLOW STRIP ===== */} +
+ {stages.map((stage, i) => ( +
+
+
+ {stage.state === "done" ? ( + + ) : ( + stage.n + )} +
+ {stage.label} +
+ {i < stages.length - 1 &&
} +
+ ))} +
+ + {/* ===== TWO-COLUMN PANELS ===== */} +
+ {/* LEFT — Query + Context */} +
+ {/* Query panel */} +
+
+
+
+ Query +
+
+ Paste the ticket or describe the issue +
+
+
+
+
+