Skip to content

Commit a48329b

Browse files
committed
[dashboard] - support in dashboard (#7608)
<!-- ## title your PR with this format: "[SDK/Dashboard/Portal] Feature/Fix: Concise title for the changes" If you did not copy the branch name from Linear, paste the issue tag here (format is TEAM-0000): ## Notes for the reviewer Anything important to call out? Be sure to also clarify these in your comments. ## How to test Unit tests, playground, etc. --> <!-- start pr-codex --> --- ## PR-Codex overview This PR focuses on restructuring the support ticketing system by moving components and modifying links to streamline support interactions within the application. ### Detailed summary - Deleted multiple support-related files and components. - Changed `getTeamById` function to remove the `export` keyword. - Updated support links to point to `/team/~/~/support`. - Added new support ticket types and forms. - Introduced new components for handling support tickets. - Implemented ticket status handling and filtering. - Enhanced the support case list display with filtering options. - Created API endpoints for support ticket management. - Integrated dynamic imports for various support forms. > The following files were skipped due to too many changes: `apps/dashboard/src/@/components/chat/CustomChatContent.tsx`, `apps/dashboard/src/@/components/chat/CustomChats.tsx`, `apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/SupportCaseDetails.tsx`, `apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/CreateSupportCase.tsx` > ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}` <!-- end pr-codex --> <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Unified support ticket system integrated into team dashboards with AI chat assistant and dynamic product-specific support forms. * Support ticket listing with status filtering, search, detailed conversation views, and reply functionality. * Added "Support" link in team sidebar footer; updated support headers, layouts, and navigation flows. * Introduced monochrome mode for mini logo and refined chat UI elements. * **Bug Fixes / Improvements** * Enhanced chat component styling and user experience. * Removed obsolete "Support" links from navigation menus and headers. * Updated button labels, including changing "Ask AI Assistant" to "Get Help". * **Chores** * Removed legacy support ticket creation pages, components, and client/server logic to consolidate support workflows. * Cleaned deprecated components, imports, redirects, and updated error messages to point to the new support section. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent 6e62a3a commit a48329b

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

59 files changed

+2085
-1158
lines changed

apps/dashboard/redirects.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -448,6 +448,11 @@ async function redirects() {
448448
...legacyDashboardToTeamRedirects,
449449
...projectPageRedirects,
450450
...teamPageRedirects,
451+
{
452+
source: "/support/:path*",
453+
destination: "/team/~/~/support",
454+
permanent: false,
455+
},
451456
];
452457
}
453458

apps/dashboard/src/@/api/support.ts

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
"use server";
2+
import { NEXT_PUBLIC_THIRDWEB_API_HOST } from "@/constants/public-envs";
3+
import type { SupportTicket } from "../../app/(app)/team/[team_slug]/(team)/~/support/types/tickets";
4+
import { getAuthToken, getAuthTokenWalletAddress } from "./auth-token";
5+
6+
const ESCALATION_FEEDBACK_RATING = 9999;
7+
8+
export async function createSupportTicket(params: {
9+
message: string;
10+
teamSlug: string;
11+
teamId: string;
12+
title: string;
13+
conversationId?: string;
14+
}): Promise<{ data: SupportTicket } | { error: string }> {
15+
const token = await getAuthToken();
16+
if (!token) {
17+
return { error: "No auth token available" };
18+
}
19+
20+
try {
21+
const walletAddress = await getAuthTokenWalletAddress();
22+
23+
const encodedTeamSlug = encodeURIComponent(params.teamSlug);
24+
const apiUrl = `${NEXT_PUBLIC_THIRDWEB_API_HOST}/v1/teams/${encodedTeamSlug}/support-conversations`;
25+
26+
// Build the payload for creating a conversation
27+
// If the message does not already include wallet address, prepend it
28+
let message = params.message;
29+
if (!message.includes("Wallet address:")) {
30+
message = `Wallet address: ${String(walletAddress || "-")}\n${message}`;
31+
}
32+
33+
const payload = {
34+
markdown: message.trim(),
35+
title: params.title,
36+
};
37+
38+
const body = JSON.stringify(payload);
39+
const headers: Record<string, string> = {
40+
Accept: "application/json",
41+
Authorization: `Bearer ${token}`,
42+
"Content-Type": "application/json",
43+
"Accept-Encoding": "identity",
44+
};
45+
46+
const response = await fetch(apiUrl, {
47+
body,
48+
headers,
49+
method: "POST",
50+
});
51+
52+
if (!response.ok) {
53+
const errorText = await response.text();
54+
return { error: `API Server error: ${response.status} - ${errorText}` };
55+
}
56+
57+
const createdConversation: SupportTicket = await response.json();
58+
59+
// Escalate to SIWA feedback endpoint if conversationId is provided
60+
if (params.conversationId) {
61+
try {
62+
const siwaUrl = process.env.NEXT_PUBLIC_SIWA_URL;
63+
if (siwaUrl) {
64+
await fetch(`${siwaUrl}/v1/chat/feedback`, {
65+
method: "POST",
66+
headers: {
67+
"Content-Type": "application/json",
68+
Authorization: `Bearer ${token}`,
69+
...(params.teamId ? { "x-team-id": params.teamId } : {}),
70+
},
71+
body: JSON.stringify({
72+
conversationId: params.conversationId,
73+
feedbackRating: ESCALATION_FEEDBACK_RATING,
74+
}),
75+
});
76+
}
77+
} catch (error) {
78+
// Log error but don't fail the ticket creation
79+
console.error("Failed to escalate to SIWA feedback:", error);
80+
}
81+
}
82+
83+
return { data: createdConversation };
84+
} catch (error) {
85+
return {
86+
error: `Failed to create support ticket: ${error instanceof Error ? error.message : "Unknown error"}`,
87+
};
88+
}
89+
}
90+
91+
export async function sendMessageToTicket(request: {
92+
ticketId: string;
93+
teamSlug: string;
94+
teamId: string;
95+
message: string;
96+
}): Promise<{ success: true } | { error: string }> {
97+
if (!request.ticketId || !request.teamSlug) {
98+
return { error: "Ticket ID and team slug are required" };
99+
}
100+
101+
const token = await getAuthToken();
102+
if (!token) {
103+
return { error: "No auth token available" };
104+
}
105+
106+
try {
107+
const encodedTeamSlug = encodeURIComponent(request.teamSlug);
108+
const encodedTicketId = encodeURIComponent(request.ticketId);
109+
const apiUrl = `${NEXT_PUBLIC_THIRDWEB_API_HOST}/v1/teams/${encodedTeamSlug}/support-conversations/${encodedTicketId}/messages`;
110+
111+
// Append /unthread send for customer messages to ensure proper routing
112+
const messageWithUnthread = `${request.message.trim()}\n/unthread send`;
113+
const payload = {
114+
markdown: messageWithUnthread,
115+
};
116+
117+
const body = JSON.stringify(payload);
118+
const headers: Record<string, string> = {
119+
Accept: "application/json",
120+
Authorization: `Bearer ${token}`,
121+
"Content-Type": "application/json",
122+
"Accept-Encoding": "identity",
123+
...(request.teamId ? { "x-team-id": request.teamId } : {}),
124+
};
125+
126+
const response = await fetch(apiUrl, {
127+
body,
128+
headers,
129+
method: "POST",
130+
});
131+
132+
if (!response.ok) {
133+
const errorText = await response.text();
134+
return { error: `API Server error: ${response.status} - ${errorText}` };
135+
}
136+
137+
return { success: true };
138+
} catch (error) {
139+
return {
140+
error: `Failed to send message: ${error instanceof Error ? error.message : "Unknown error"}`,
141+
};
142+
}
143+
}

apps/dashboard/src/@/api/team.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ export async function service_getTeamBySlug(slug: string) {
4848
return null;
4949
}
5050

51-
export function getTeamById(id: string) {
51+
function getTeamById(id: string) {
5252
return getTeamBySlug(id);
5353
}
5454

apps/dashboard/src/@/components/chat/ChatBar.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,13 +116,12 @@ export function ChatBar(props: {
116116
) : (
117117
<Button
118118
aria-label="Send"
119-
className="!h-auto w-auto border border-nebula-pink-foreground p-2 disabled:opacity-100"
119+
className="!h-auto w-auto p-2 disabled:opacity-100"
120120
disabled={message.trim() === "" || props.isConnectingWallet}
121121
onClick={() => {
122122
if (message.trim() === "") return;
123123
handleSubmit(message);
124124
}}
125-
variant="pink"
126125
>
127126
<ArrowUpIcon className="size-4" />
128127
</Button>

apps/dashboard/src/@/components/chat/CustomChatButton.tsx

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import { MessageCircleIcon, XIcon } from "lucide-react";
44
import { useCallback, useRef, useState } from "react";
55
import { createThirdwebClient } from "thirdweb";
6+
import type { Team } from "@/api/team";
67
import { Button } from "@/components/ui/button";
78
import { NEXT_PUBLIC_DASHBOARD_CLIENT_ID } from "@/constants/public-envs";
89
import { cn } from "@/lib/utils";
@@ -14,16 +15,11 @@ const client = createThirdwebClient({
1415
});
1516

1617
export function CustomChatButton(props: {
17-
isLoggedIn: boolean;
18-
networks: "mainnet" | "testnet" | "all" | null;
19-
isFloating: boolean;
20-
pageType: "chain" | "contract" | "support";
2118
label: string;
2219
examplePrompts: string[];
23-
authToken: string | undefined;
24-
teamId: string | undefined;
20+
authToken: string;
21+
team: Team;
2522
clientId: string | undefined;
26-
requireLogin?: boolean;
2723
}) {
2824
const [isOpen, setIsOpen] = useState(false);
2925
const [hasBeenOpened, setHasBeenOpened] = useState(false);
@@ -54,14 +50,14 @@ export function CustomChatButton(props: {
5450
ref={ref}
5551
>
5652
{/* Header with close button */}
57-
<div className="flex items-center justify-between border-b px-4 py-2">
53+
<div className="flex items-center justify-between border-b px-4 py-4">
5854
<div className="flex items-center gap-2 font-semibold text-lg">
5955
<MessageCircleIcon className="size-5 text-muted-foreground" />
6056
{props.label}
6157
</div>
6258
<Button
6359
aria-label="Close chat"
64-
className="h-auto w-auto p-1 text-muted-foreground"
60+
className="h-auto w-auto p-1 text-muted-foreground rounded-full"
6561
onClick={closeModal}
6662
size="icon"
6763
variant="ghost"
@@ -80,9 +76,7 @@ export function CustomChatButton(props: {
8076
message: prompt,
8177
title: prompt,
8278
}))}
83-
networks={props.networks}
84-
requireLogin={props.requireLogin}
85-
teamId={props.teamId}
79+
team={props.team}
8680
/>
8781
)}
8882
</div>

0 commit comments

Comments
 (0)