feat(ai-widget): Copy Cover Letter button + draft_content plumbing
All checks were successful
build-and-release / build (push) Successful in 1m46s

- ChatMessage now carries draftContent?: string (from API draft_content field)
- ChatResponse interface extended with draft_content?: string
- sendMessage populates draftContent on assistant messages
- confirmSaveProfile uses draftContent directly (no more regex parsing)
- 'Save to Profile' button guards on msg.draftContent being truthy
- New blue 'Copy Cover Letter' button for cover_letter_draft messages:
    copies msg.draftContent to clipboard, marks message confirmed,
    appends a confirmation assistant message
- 'Open Cover Letter Tool →' nav link still shows alongside the Copy button
  via the existing navUrl('open_cover_letter') path

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Tracewebstudio Dev 2026-08-15 13:16:29 +02:00
parent 106561189f
commit ff2dde1900

View file

@ -9,10 +9,12 @@ interface ChatMessage {
intent?: string; intent?: string;
status?: string; status?: string;
suggestedAction?: string; suggestedAction?: string;
/** Semantic category: ticket_created | ticket_pending | kb_results | usage_info | navigation */ /** Semantic category: ticket_created | ticket_pending | kb_results | usage_info | navigation | profile_draft | cover_letter_draft */
actionType?: string; actionType?: string;
/** Original user text — stored so ticket confirm has a description to send */ /** Original user text — stored so ticket confirm has a description to send */
userQuery?: string; userQuery?: string;
/** Raw generated content (profile draft, cover letter body) for save/copy actions */
draftContent?: string;
/** True once the user has clicked the confirm/action button on this message */ /** True once the user has clicked the confirm/action button on this message */
confirmed?: boolean; confirmed?: boolean;
} }
@ -29,6 +31,8 @@ interface ChatResponse {
status?: string; status?: string;
suggested_action?: string; suggested_action?: string;
action_type?: string; action_type?: string;
/** Raw generated text (profile draft, cover letter body) for copy/save actions */
draft_content?: string;
} }
function statusLabel(status?: string): string | null { function statusLabel(status?: string): string | null {
@ -195,6 +199,7 @@ export function AiChatWidget() {
status: data.status, status: data.status,
suggestedAction: data.suggested_action, suggestedAction: data.suggested_action,
actionType: data.action_type, actionType: data.action_type,
draftContent: data.draft_content,
userQuery: text, userQuery: text,
confirmed: false, confirmed: false,
}; };
@ -280,10 +285,8 @@ export function AiChatWidget() {
}; };
/** Called when user clicks "Save to Profile" on a profile_draft message. */ /** Called when user clicks "Save to Profile" on a profile_draft message. */
const confirmSaveProfile = async (msgIdx: number, draftText: string) => { const confirmSaveProfile = async (msgIdx: number, draftContent: string) => {
// Extract just the quoted improved text if it's wrapped in our "Here's an improved..." message const cleanDraft = draftContent;
const quotedMatch = draftText.match(/"([^"]+)"/);
const cleanDraft = quotedMatch ? quotedMatch[1] : draftText;
setMessages((prev) => prev.map((m, i) => (i === msgIdx ? { ...m, confirmed: true } : m))); setMessages((prev) => prev.map((m, i) => (i === msgIdx ? { ...m, confirmed: true } : m)));
setIsLoading(true); setIsLoading(true);
@ -591,9 +594,9 @@ export function AiChatWidget() {
</Show> </Show>
{/* Save-profile button — for profile_draft messages */} {/* Save-profile button — for profile_draft messages */}
<Show when={msg.actionType === "profile_draft"}> <Show when={msg.actionType === "profile_draft" && msg.draftContent}>
<button <button
onClick={() => confirmSaveProfile(idx(), msg.content)} onClick={() => confirmSaveProfile(idx(), msg.draftContent!)}
style={{ style={{
display: "inline-flex", display: "inline-flex",
"align-items": "center", "align-items": "center",
@ -610,6 +613,42 @@ export function AiChatWidget() {
Save to Profile Save to Profile
</button> </button>
</Show> </Show>
{/* Copy button — for cover_letter_draft messages */}
<Show when={msg.actionType === "cover_letter_draft" && msg.draftContent}>
<button
onClick={() => {
navigator.clipboard.writeText(msg.draftContent!).then(() => {
// Mark confirmed so the buttons disappear after copy
setMessages((prev) =>
prev.map((m, i) => (i === idx() ? { ...m, confirmed: true } : m))
);
setMessages((prev) => [
...prev,
{
role: "assistant" as const,
content: "Cover letter copied to clipboard! Paste it into your application and tailor it to the specific role.",
confirmed: true,
},
]);
});
}}
style={{
display: "inline-flex",
"align-items": "center",
padding: "5px 12px",
background: "#2563EB",
border: "none",
"border-radius": "20px",
color: "#fff",
"font-size": "11px",
"font-weight": "600",
cursor: "pointer",
}}
>
Copy Cover Letter
</button>
</Show>
</div> </div>
</Show> </Show>
</> </>