diff --git a/apps/supportconsolek/supportconsolek/.env.example b/apps/supportconsolek/supportconsolek/.env.example new file mode 100644 index 0000000..bc164a3 --- /dev/null +++ b/apps/supportconsolek/supportconsolek/.env.example @@ -0,0 +1,4 @@ +DATABRICKS_HOST=https://... +DATABRICKS_APP_PORT=8000 +DATABRICKS_APP_NAME=caspers-supportconsole +FLASK_RUN_HOST=0.0.0.0 diff --git a/apps/supportconsolek/supportconsolek/.gitignore b/apps/supportconsolek/supportconsolek/.gitignore new file mode 100644 index 0000000..f2abc32 --- /dev/null +++ b/apps/supportconsolek/supportconsolek/.gitignore @@ -0,0 +1,10 @@ +.DS_Store +node_modules/ +client/dist/ +dist/ +build/ +.env +.databricks/ +.smoke-test/ +test-results/ +playwright-report/ diff --git a/apps/supportconsolek/supportconsolek/.prettierignore b/apps/supportconsolek/supportconsolek/.prettierignore new file mode 100644 index 0000000..7d3d77c --- /dev/null +++ b/apps/supportconsolek/supportconsolek/.prettierignore @@ -0,0 +1,36 @@ +# Dependencies +node_modules + +# Build outputs +dist +build +client/dist +.next +.databricks/ + +# Environment files +.env +.env.local +.env.*.local + +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Coverage +coverage + +# Cache +.cache +.turbo + +# Lock files +package-lock.json +yarn.lock +pnpm-lock.yaml + +# Vendor +vendor diff --git a/apps/supportconsolek/supportconsolek/.prettierrc.json b/apps/supportconsolek/supportconsolek/.prettierrc.json new file mode 100644 index 0000000..d95a63f --- /dev/null +++ b/apps/supportconsolek/supportconsolek/.prettierrc.json @@ -0,0 +1,12 @@ +{ + "semi": true, + "trailingComma": "es5", + "singleQuote": true, + "printWidth": 120, + "tabWidth": 2, + "useTabs": false, + "arrowParens": "always", + "endOfLine": "lf", + "bracketSpacing": true, + "jsxSingleQuote": false +} diff --git a/apps/supportconsolek/supportconsolek/app.yaml b/apps/supportconsolek/supportconsolek/app.yaml new file mode 100644 index 0000000..95576a0 --- /dev/null +++ b/apps/supportconsolek/supportconsolek/app.yaml @@ -0,0 +1,14 @@ +command: ['npm', 'run', 'start'] +env: + - name: PGPORT + value: "5432" + - name: PGDATABASE + value: "databricks_postgres" + - name: PGSSLMODE + value: "require" + - name: LAKEBASE_ENDPOINT + value: "projects/casperskitchens-support-db/branches/production/endpoints/primary" + - name: DATABRICKS_WAREHOUSE_ID + value: "e5ed18828056f3cf" + - name: SUPPORT_AGENT_ENDPOINT_NAME + value: "caspers_support_agent" \ No newline at end of file diff --git a/apps/supportconsolek/supportconsolek/appkit.plugins.json b/apps/supportconsolek/supportconsolek/appkit.plugins.json new file mode 100644 index 0000000..67f3874 --- /dev/null +++ b/apps/supportconsolek/supportconsolek/appkit.plugins.json @@ -0,0 +1,41 @@ +{ + "$schema": "https://databricks.github.io/appkit/schemas/template-plugins.schema.json", + "version": "1.0", + "plugins": { + "analytics": { + "name": "analytics", + "displayName": "Analytics Plugin", + "description": "SQL query execution against Databricks SQL Warehouses", + "package": "@databricks/appkit", + "resources": { + "required": [ + { + "type": "sql_warehouse", + "alias": "SQL Warehouse", + "resourceKey": "sql-warehouse", + "description": "SQL Warehouse for executing analytics queries", + "permission": "CAN_USE", + "fields": { + "id": { + "env": "DATABRICKS_WAREHOUSE_ID", + "description": "SQL Warehouse ID" + } + } + } + ], + "optional": [] + } + }, + "server": { + "name": "server", + "displayName": "Server Plugin", + "description": "HTTP server with Express, static file serving, and Vite dev mode support", + "package": "@databricks/appkit", + "requiredByTemplate": true, + "resources": { + "required": [], + "optional": [] + } + } + } +} diff --git a/apps/supportconsolek/supportconsolek/client/components.json b/apps/supportconsolek/supportconsolek/client/components.json new file mode 100644 index 0000000..13e1db0 --- /dev/null +++ b/apps/supportconsolek/supportconsolek/client/components.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "", + "css": "src/index.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "iconLibrary": "lucide" +} diff --git a/apps/supportconsolek/supportconsolek/client/index.html b/apps/supportconsolek/supportconsolek/client/index.html new file mode 100644 index 0000000..26f3416 --- /dev/null +++ b/apps/supportconsolek/supportconsolek/client/index.html @@ -0,0 +1,18 @@ + + + + + + + + + + + + caspers-supportconsole + + +
+ + + diff --git a/apps/supportconsolek/supportconsolek/client/postcss.config.js b/apps/supportconsolek/supportconsolek/client/postcss.config.js new file mode 100644 index 0000000..51a6e4e --- /dev/null +++ b/apps/supportconsolek/supportconsolek/client/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + '@tailwindcss/postcss': {}, + autoprefixer: {}, + }, +}; diff --git a/apps/supportconsolek/supportconsolek/client/public/apple-touch-icon.png b/apps/supportconsolek/supportconsolek/client/public/apple-touch-icon.png new file mode 100644 index 0000000..32053bd Binary files /dev/null and b/apps/supportconsolek/supportconsolek/client/public/apple-touch-icon.png differ diff --git a/apps/supportconsolek/supportconsolek/client/public/favicon-16x16.png b/apps/supportconsolek/supportconsolek/client/public/favicon-16x16.png new file mode 100644 index 0000000..d7c16eb Binary files /dev/null and b/apps/supportconsolek/supportconsolek/client/public/favicon-16x16.png differ diff --git a/apps/supportconsolek/supportconsolek/client/public/favicon-192x192.png b/apps/supportconsolek/supportconsolek/client/public/favicon-192x192.png new file mode 100644 index 0000000..8b4f18d Binary files /dev/null and b/apps/supportconsolek/supportconsolek/client/public/favicon-192x192.png differ diff --git a/apps/supportconsolek/supportconsolek/client/public/favicon-32x32.png b/apps/supportconsolek/supportconsolek/client/public/favicon-32x32.png new file mode 100644 index 0000000..46aa684 Binary files /dev/null and b/apps/supportconsolek/supportconsolek/client/public/favicon-32x32.png differ diff --git a/apps/supportconsolek/supportconsolek/client/public/favicon-48x48.png b/apps/supportconsolek/supportconsolek/client/public/favicon-48x48.png new file mode 100644 index 0000000..d2f89fb Binary files /dev/null and b/apps/supportconsolek/supportconsolek/client/public/favicon-48x48.png differ diff --git a/apps/supportconsolek/supportconsolek/client/public/favicon-512x512.png b/apps/supportconsolek/supportconsolek/client/public/favicon-512x512.png new file mode 100644 index 0000000..14d7d20 Binary files /dev/null and b/apps/supportconsolek/supportconsolek/client/public/favicon-512x512.png differ diff --git a/apps/supportconsolek/supportconsolek/client/public/favicon.svg b/apps/supportconsolek/supportconsolek/client/public/favicon.svg new file mode 100644 index 0000000..cb30c1e --- /dev/null +++ b/apps/supportconsolek/supportconsolek/client/public/favicon.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/apps/supportconsolek/supportconsolek/client/public/site.webmanifest b/apps/supportconsolek/supportconsolek/client/public/site.webmanifest new file mode 100644 index 0000000..03106ce --- /dev/null +++ b/apps/supportconsolek/supportconsolek/client/public/site.webmanifest @@ -0,0 +1,19 @@ +{ + "name": "{{.project_name}}", + "short_name": "{{.project_name}}", + "icons": [ + { + "src": "/favicon-192x192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "/favicon-512x512.png", + "sizes": "512x512", + "type": "image/png" + } + ], + "theme_color": "#ffffff", + "background_color": "#ffffff", + "display": "standalone" +} diff --git a/apps/supportconsolek/supportconsolek/client/src/App.tsx b/apps/supportconsolek/supportconsolek/client/src/App.tsx new file mode 100644 index 0000000..aa5ea08 --- /dev/null +++ b/apps/supportconsolek/supportconsolek/client/src/App.tsx @@ -0,0 +1,860 @@ +/** + * ⚠️ BEFORE MODIFYING THIS FILE: + * + * 1. Create SQL files in config/queries/ + * 2. Run `npm run typegen` to generate query types + * 3. Check appKitTypes.d.ts for available types + * + * Common Mistakes: + * - DataTable does NOT accept `data` or `columns` props + * - Charts use `xKey` and `yKey`, NOT `seriesKey`/`nameKey`/`valueKey` + * - useAnalyticsQuery has no `enabled` option - use conditional rendering + */ +import { useEffect, useRef, useState } from "react"; +import { + Badge, + Button, + Card, + CardContent, + CardHeader, + CardTitle, + Input, + Sheet, + SheetClose, + SheetContent, + SheetDescription, + SheetFooter, + SheetHeader, + SheetTitle, + Textarea, +} from "@databricks/appkit-ui/react"; +import { Toaster, toast } from "sonner"; + +type Summary = { requests: number; actions: number; replies: number }; + +type Recommendation = { + amount_usd?: number | null; + reason?: string; +}; + +type Report = { + draft_response?: string; + past_interactions_summary?: string; + order_details_summary?: string; + decision_confidence?: string; + escalation_flag?: boolean; + refund_recommendation?: Recommendation | null; + credit_recommendation?: Recommendation | null; +}; + +type CaseState = { + case_status: "pending" | "in_progress" | "done" | "blocked"; + next_action: string; + has_reply: boolean; + has_refund: boolean; + has_credit: boolean; + action_count: number; + reply_count: number; + regen_count: number; + last_action_type?: string | null; + last_event_at?: string | null; + latest_report_source?: string | null; +}; + +type TimelineEvent = { + event_type: string; + event_at: string; + actor?: string | null; + details?: Record; +}; + +type RegenerationItem = { + regenerated_report_id: number; + operator_context?: string | null; + actor?: string | null; + created_at: string; + report: Report; +}; + +type ResponseRating = { + rating_id: number; + rating: "thumbs_up" | "thumbs_down"; + reason_code?: string | null; + feedback_notes?: string | null; + actor?: string | null; + created_at: string; +}; + +type RequestItem = { + support_request_id: string; + user_id: string; + user_display_name?: string | null; + order_id: string; + ts: string; + request_text?: string | null; + report: Report; + case_state?: CaseState; +}; + +type RequestDetails = RequestItem & { + actions: Array>; + replies: Array>; + ratings?: ResponseRating[]; + latest_rating?: ResponseRating | null; + regenerations?: RegenerationItem[]; + timeline?: TimelineEvent[]; +}; + +type NoticeState = { + kind: "success" | "error"; + message: string; + supportRequestId?: string; +} | null; + +const RATING_REASON_OPTIONS: Array<{ value: string; label: string }> = [ + { value: "incorrect_facts", label: "Incorrect facts in response" }, + { value: "wrong_refund_amount", label: "Wrong refund amount" }, + { value: "wrong_credit_amount", label: "Wrong credit amount" }, + { value: "should_escalate", label: "Should have escalated" }, + { value: "should_not_escalate", label: "Should not have escalated" }, + { value: "poor_tone", label: "Poor tone or wording" }, + { value: "unclear_response", label: "Unclear or incomplete response" }, + { value: "other", label: "Other" }, +]; + +function App() { + const PAGE_SIZE = 50; + const [summary, setSummary] = useState(null); + const [requests, setRequests] = useState([]); + const [totalRequests, setTotalRequests] = useState(0); + const [currentPage, setCurrentPage] = useState(1); + const [selected, setSelected] = useState(null); + const [replyText, setReplyText] = useState(""); + const [operatorContext, setOperatorContext] = useState(""); + const [actor, setActor] = useState(""); + const [refundAmount, setRefundAmount] = useState(""); + const [creditAmount, setCreditAmount] = useState(""); + const [ratingChoice, setRatingChoice] = useState<"thumbs_up" | "thumbs_down">("thumbs_up"); + const [ratingReason, setRatingReason] = useState(""); + const [ratingNotes, setRatingNotes] = useState(""); + const [loading, setLoading] = useState(false); + const [detailsLoading, setDetailsLoading] = useState(false); + const [isDrawerOpen, setIsDrawerOpen] = useState(false); + const [pendingAction, setPendingAction] = useState<"apply_refund" | "apply_credit" | "send_reply" | "regenerate" | "rate_response" | null>(null); + const [notice, setNotice] = useState(null); + const [error, setError] = useState(null); + const drawerScrollRef = useRef(null); + + useEffect(() => { + void refresh(); + }, [currentPage]); + + const formatCurrency = (value?: number | null) => + typeof value === "number" ? `$${value.toFixed(2)}` : "No recommendation"; + + const formatTs = (value: string) => { + const date = new Date(value); + if (Number.isNaN(date.getTime())) { + return "Recent"; + } + return date.toLocaleString(); + }; + + const statusLabel = (status: CaseState["case_status"] | undefined) => { + if (status === "done") return "Done"; + if (status === "in_progress") return "In Progress"; + if (status === "blocked") return "Blocked"; + return "Pending"; + }; + + const statusVariant = (status: CaseState["case_status"] | undefined): "default" | "secondary" | "outline" => { + if (status === "done") return "default"; + if (status === "in_progress") return "secondary"; + return "outline"; + }; + + const nextActionLabel = (nextAction: string | undefined) => { + const mapping: Record = { + review_report: "Review report", + apply_resolution_or_regenerate: "Apply resolution or re-gen", + send_customer_reply: "Send customer reply", + monitor: "Monitor case", + investigate_blocker: "Investigate blocker", + continue_investigation: "Continue investigation", + }; + return mapping[nextAction ?? ""] ?? "Review report"; + }; + + const suggestedRefund = selected?.report?.refund_recommendation?.amount_usd ?? null; + const suggestedCredit = selected?.report?.credit_recommendation?.amount_usd ?? null; + const selectedCaseState = selected?.case_state; + const latestRating = selected?.latest_rating; + const latestRatingLabel = latestRating?.rating === "thumbs_up" + ? "Agent Rating: Thumbs Up" + : latestRating?.rating === "thumbs_down" + ? "Agent Rating: Thumbs Down" + : "Agent Rating: Not rated"; + + const appliedRefund = selected?.actions.find((a) => a.action_type === "apply_refund"); + const appliedCredit = selected?.actions.find((a) => a.action_type === "apply_credit"); + + const parseErrorMessage = async (res: Response): Promise => { + try { + const body = (await res.json()) as { message?: string; error?: string }; + return body.message || body.error || `Request failed (${res.status})`; + } catch { + return `Request failed (${res.status})`; + } + }; + + const showNotice = (nextNotice: Exclude) => { + setNotice(nextNotice); + if (nextNotice.kind === "success") { + toast.success(nextNotice.message); + } else { + toast.error(nextNotice.message); + } + }; + + const refresh = async () => { + setLoading(true); + setError(null); + try { + const offset = (currentPage - 1) * PAGE_SIZE; + const [summaryRes, reqRes] = await Promise.all([ + fetch("/api/support/summary"), + fetch(`/api/support/requests?limit=${PAGE_SIZE}&offset=${offset}`), + ]); + if (!summaryRes.ok || !reqRes.ok) { + throw new Error("Failed to load support data"); + } + const summaryJson = (await summaryRes.json()) as Summary; + const reqJson = (await reqRes.json()) as { items?: RequestItem[]; total?: number }; + setSummary(summaryJson); + setRequests(reqJson.items || []); + setTotalRequests(reqJson.total ?? 0); + } catch (e) { + setError(e instanceof Error ? e.message : String(e)); + } finally { + setLoading(false); + } + }; + + const openDetails = async ( + supportRequestId: string, + options?: { showLoading?: boolean; preserveScroll?: boolean }, + ) => { + const showLoading = options?.showLoading ?? true; + const preserveScroll = options?.preserveScroll ?? false; + const previousScrollTop = preserveScroll ? drawerScrollRef.current?.scrollTop ?? 0 : 0; + setIsDrawerOpen(true); + if (showLoading) { + setDetailsLoading(true); + } + setNotice((prev) => (prev?.supportRequestId === supportRequestId ? prev : null)); + try { + const res = await fetch(`/api/support/requests/${supportRequestId}`); + if (!res.ok) { + throw new Error("Failed to load request details"); + } + const json = (await res.json()) as RequestDetails; + setSelected(json); + setReplyText(json?.report?.draft_response ?? ""); + setRefundAmount( + typeof json?.report?.refund_recommendation?.amount_usd === "number" + ? String(json.report.refund_recommendation.amount_usd) + : "", + ); + setCreditAmount( + typeof json?.report?.credit_recommendation?.amount_usd === "number" + ? String(json.report.credit_recommendation.amount_usd) + : "", + ); + setRatingChoice(json?.latest_rating?.rating === "thumbs_down" ? "thumbs_down" : "thumbs_up"); + setRatingReason(json?.latest_rating?.reason_code ?? ""); + setRatingNotes(json?.latest_rating?.feedback_notes ?? ""); + setError(null); + if (preserveScroll) { + requestAnimationFrame(() => { + if (drawerScrollRef.current) { + drawerScrollRef.current.scrollTop = previousScrollTop; + } + }); + } + return true; + } catch (e) { + setError(e instanceof Error ? e.message : String(e)); + return false; + } finally { + if (showLoading) { + setDetailsLoading(false); + } + } + }; + + const totalPages = Math.max(1, Math.ceil(totalRequests / PAGE_SIZE)); + + const applyAction = async (actionType: "apply_refund" | "apply_credit", amount: string) => { + if (!selected) return; + if (!amount || Number.isNaN(Number(amount))) return; + setPendingAction(actionType); + setNotice(null); + try { + const res = await fetch("/api/support/actions", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + support_request_id: selected.support_request_id, + order_id: selected.order_id, + user_id: selected.user_id, + action_type: actionType, + amount_usd: Number(amount), + actor: actor || null, + payload: { source: "appkit-ui" }, + }), + }); + if (!res.ok) { + throw new Error(await parseErrorMessage(res)); + } + showNotice({ + kind: "success", + message: actionType === "apply_credit" ? "Credits applied successfully." : "Refund applied successfully.", + supportRequestId: selected.support_request_id, + }); + await openDetails(selected.support_request_id, { showLoading: false, preserveScroll: true }); + await refresh(); + } catch (e) { + showNotice({ + kind: "error", + message: e instanceof Error ? e.message : String(e), + }); + } finally { + setPendingAction(null); + } + }; + + const sendReply = async () => { + if (!selected) return; + setPendingAction("send_reply"); + setNotice(null); + try { + const res = await fetch("/api/support/replies", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + support_request_id: selected.support_request_id, + order_id: selected.order_id, + user_id: selected.user_id, + message_text: replyText, + sent_by: actor || null, + }), + }); + if (!res.ok) { + throw new Error(await parseErrorMessage(res)); + } + showNotice({ + kind: "success", + message: "Reply sent successfully.", + supportRequestId: selected.support_request_id, + }); + await openDetails(selected.support_request_id, { showLoading: false, preserveScroll: true }); + await refresh(); + } catch (e) { + showNotice({ + kind: "error", + message: e instanceof Error ? e.message : String(e), + }); + } finally { + setPendingAction(null); + } + }; + + const regenerateReport = async () => { + if (!selected) return; + setPendingAction("regenerate"); + setNotice(null); + try { + const res = await fetch("/api/support/regenerate", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + support_request_id: selected.support_request_id, + order_id: selected.order_id, + user_id: selected.user_id, + actor: actor || null, + operator_context: operatorContext || null, + current_report: selected.report, + }), + }); + if (!res.ok) { + throw new Error(await parseErrorMessage(res)); + } + const body = (await res.json()) as { warning?: string }; + showNotice({ + kind: "success", + message: body.warning + ? `Report regenerated with fallback. ${body.warning}` + : "Report regenerated with operator context.", + supportRequestId: selected.support_request_id, + }); + await openDetails(selected.support_request_id, { showLoading: false, preserveScroll: true }); + await refresh(); + } catch (e) { + showNotice({ + kind: "error", + message: e instanceof Error ? e.message : String(e), + }); + } finally { + setPendingAction(null); + } + }; + + const submitRating = async () => { + if (!selected) return; + setPendingAction("rate_response"); + setNotice(null); + try { + const res = await fetch("/api/support/ratings", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + support_request_id: selected.support_request_id, + order_id: selected.order_id, + user_id: selected.user_id, + rating: ratingChoice, + reason_code: ratingReason || null, + feedback_notes: ratingNotes || null, + actor: actor || null, + }), + }); + if (!res.ok) { + throw new Error(await parseErrorMessage(res)); + } + showNotice({ + kind: "success", + message: "Agent response rating saved.", + supportRequestId: selected.support_request_id, + }); + await openDetails(selected.support_request_id, { showLoading: false, preserveScroll: true }); + await refresh(); + } catch (e) { + showNotice({ + kind: "error", + message: e instanceof Error ? e.message : String(e), + }); + } finally { + setPendingAction(null); + } + }; + + return ( +
+ +
+
+

Support Console

+

+ Triage support requests, review agent analysis, and take operator actions. +

+
+ +
+ + {error && ( + + Error: {error} + + )} + +
+ + + Requests + + +
{summary?.requests ?? "-"}
+
+
+ + + Actions + + +
{summary?.actions ?? "-"}
+
+
+ + + Replies + + +
{summary?.replies ?? "-"}
+
+
+
+ +
+ + + Support Requests + + + {loading && requests.length === 0 && ( + <> + {Array.from({ length: 4 }).map((_, idx) => ( +
+
+
+
+
+ ))} + + )} + {requests.length === 0 && ( +
+ No support requests yet. +
+ )} + {requests.map((r) => { + const previewSource = + r.report?.draft_response || + r.report?.order_details_summary || + "Support request ready for review."; + const preview = previewSource.replace(/\s+/g, " ").trim(); + return ( +
+
+
+
+ {r.user_display_name ?? "Customer"} +
+ + {statusLabel(r.case_state?.case_status)} + + + Next: {nextActionLabel(r.case_state?.next_action)} + + {r.case_state?.has_reply && Replied} + {r.case_state?.has_refund && Refund Applied} + {r.case_state?.has_credit && Credits Applied} + + Refund: {formatCurrency(r.report?.refund_recommendation?.amount_usd)} + + + Credit: {formatCurrency(r.report?.credit_recommendation?.amount_usd)} + +
+
+ Updated {formatTs(r.case_state?.last_event_at ?? r.ts)} + + Replies {r.case_state?.reply_count ?? 0} + + + Actions {r.case_state?.action_count ?? 0} + + + Re-gens {r.case_state?.regen_count ?? 0} + +
+
{preview}
+
+ +
+ ); + })} +
+
+ Page {currentPage} of {totalPages} +
+
+ + +
+
+ + +
+ + +
+ + Request Details + + Review agent analysis and take operator actions for the selected support case. + + +
+ {notice && ( + + + {notice.message} + + + )} + {detailsLoading &&
Loading request details...
} + {!detailsLoading && !selected && ( +
Select a request from the list.
+ )} + {!detailsLoading && selected && ( + <> +
+
+ {selected.user_display_name ?? "Customer"} + + Updated: {formatTs(selectedCaseState?.last_event_at ?? selected.ts)} + + + Confidence: {selected.report?.decision_confidence ?? "unknown"} + + + {statusLabel(selectedCaseState?.case_status)} + +
+
+
+
History
+
+ + Current Status: {statusLabel(selectedCaseState?.case_status)} + + Next: {nextActionLabel(selectedCaseState?.next_action)} + Last Action: {selectedCaseState?.last_action_type ?? "none"} + {latestRatingLabel} +
+
+ {(selected.timeline ?? []).slice(0, 8).map((event, idx) => ( +
+
{event.event_type.replaceAll("_", " ")}
+
+ {formatTs(event.event_at)}{event.actor ? ` · ${event.actor}` : ""} +
+
+ ))} + {(selected.timeline ?? []).length === 0 && ( +
No activity yet.
+ )} +
+
+ +
+
Support Request & Actions
+
+
Raw Support Message
+
+ {selected.request_text || "Raw support message not available for this request yet."} +
+
+
+
Suggested Agent Response
+