diff --git a/apps/dashboard/src/@/api/support.ts b/apps/dashboard/src/@/api/support.ts
new file mode 100644
index 00000000000..998b2d5a700
--- /dev/null
+++ b/apps/dashboard/src/@/api/support.ts
@@ -0,0 +1,354 @@
+"use server";
+import "server-only";
+import { NEXT_PUBLIC_THIRDWEB_API_HOST } from "@/constants/public-envs";
+import { getAuthToken, getAuthTokenWalletAddress } from "./auth-token";
+
+export interface SupportTicket {
+ id: string;
+ status: "needs_response" | "in_progress" | "on_hold" | "closed" | "resolved";
+ createdAt: string;
+ updatedAt: string;
+ messages?: SupportMessage[];
+}
+
+interface SupportMessage {
+ id: string;
+ content: string;
+ createdAt: string;
+ timestamp: string;
+ author?: {
+ name: string;
+ email: string;
+ type: "user" | "customer";
+ };
+}
+
+interface CreateSupportTicketRequest {
+ message: string;
+ teamSlug: string;
+ title: string;
+ conversationId?: string;
+}
+
+interface SendMessageRequest {
+ ticketId: string;
+ teamSlug: string;
+ message: string;
+}
+
+export async function getSupportTicketsByTeam(
+ teamSlug: string,
+ authToken?: string,
+): Promise {
+ if (!teamSlug) {
+ throw new Error("Team slug is required to fetch support tickets");
+ }
+
+ const token = authToken || (await getAuthToken());
+ if (!token) {
+ throw new Error("No auth token available");
+ }
+
+ // URL encode the team slug to handle special characters like #
+ const encodedTeamSlug = encodeURIComponent(teamSlug);
+ const apiUrl = `${NEXT_PUBLIC_THIRDWEB_API_HOST}/v1/teams/${encodedTeamSlug}/support-conversations/list`;
+
+ // Build the POST payload according to API spec
+ const payload = {
+ limit: 50,
+ descending: true,
+ };
+
+ const response = await fetch(apiUrl, {
+ body: JSON.stringify(payload),
+ cache: "no-store",
+ headers: {
+ Accept: "application/json",
+ "Accept-Encoding": "identity",
+ Authorization: `Bearer ${token}`,
+ "Content-Type": "application/json",
+ },
+ method: "POST",
+ });
+
+ if (!response.ok) {
+ const errorText = await response.text();
+ throw new Error(`API Server error: ${response.status} - ${errorText}`);
+ }
+ const data: { data?: SupportTicket[] } = await response.json();
+ const conversations = data.data || [];
+ return conversations;
+}
+
+interface RawSupportMessage {
+ id: string;
+ text?: string;
+ timestamp?: string;
+ createdAt?: string;
+ isPrivateNote?: boolean;
+ sentByUser?: {
+ name: string;
+ email: string;
+ isExternal: boolean;
+ };
+ // Add any other fields you use from the API
+}
+
+export async function getSupportTicket(
+ ticketId: string,
+ teamSlug: string,
+ authToken?: string,
+): Promise {
+ if (!ticketId || !teamSlug) {
+ throw new Error("Ticket ID and team slug are required");
+ }
+
+ const token = authToken || (await getAuthToken());
+ if (!token) {
+ throw new Error("No auth token available");
+ }
+
+ // URL encode the team slug to handle special characters like #
+ const encodedTeamSlug = encodeURIComponent(teamSlug);
+ const encodedTicketId = encodeURIComponent(ticketId);
+
+ const messagesPayload = {
+ limit: 100,
+ descending: false,
+ };
+
+ // Fetch conversation details and messages in parallel
+ const [conversationResponse, messagesResponse] = await Promise.all([
+ fetch(
+ `${NEXT_PUBLIC_THIRDWEB_API_HOST}/v1/teams/${encodedTeamSlug}/support-conversations/${encodedTicketId}`,
+ {
+ cache: "no-store",
+ headers: {
+ Accept: "application/json",
+ "Accept-Encoding": "identity",
+ Authorization: `Bearer ${token}`,
+ "Content-Type": "application/json",
+ },
+ method: "GET",
+ },
+ ),
+ fetch(
+ `${NEXT_PUBLIC_THIRDWEB_API_HOST}/v1/teams/${encodedTeamSlug}/support-conversations/${encodedTicketId}/messages/list`,
+ {
+ body: JSON.stringify(messagesPayload),
+ cache: "no-store",
+ headers: {
+ Accept: "application/json",
+ "Accept-Encoding": "identity",
+ Authorization: `Bearer ${token}`,
+ "Content-Type": "application/json",
+ },
+ method: "POST",
+ },
+ ),
+ ]);
+
+ if (!conversationResponse.ok) {
+ if (conversationResponse.status === 404) {
+ return null; // Ticket not found
+ }
+ const errorText = await conversationResponse.text();
+ throw new Error(
+ `API Server error: ${conversationResponse.status} - ${errorText}`,
+ );
+ }
+
+ const conversation: SupportTicket = await conversationResponse.json();
+
+ // Fetch and map messages if the messages request was successful
+ if (messagesResponse.ok) {
+ const messagesData: { data?: unknown[] } = await messagesResponse.json();
+ const rawMessages = messagesData.data || [];
+ // Transform the raw messages to match our interface
+ const messages: SupportMessage[] = (rawMessages as RawSupportMessage[])
+ .filter((msg) => {
+ // Filter out messages without content - check both text and text fields
+ const hasContent = msg.text && msg.text.length > 0;
+ const hasText = msg.text && msg.text.trim().length > 0;
+ // Filter out private notes - they should not be shown to customers
+ const isNotPrivateNote = !msg.isPrivateNote;
+ return (hasContent || hasText) && isNotPrivateNote;
+ })
+ .map((msg) => {
+ // Use text if available and is a non-empty array, otherwise fall back to text
+ let content = "";
+ if (typeof msg.text === "string" && msg.text.trim().length > 0) {
+ content = msg.text;
+ }
+
+ // Clean up 'Email:' line to show only the plain email if it contains a mailto link
+ if (content) {
+ content = content
+ .split("\n")
+ .map((line) => {
+ if (line.trim().toLowerCase().startsWith("email:")) {
+ // Extract email from
+ const match = line.match(/]+)\|[^>]+>/);
+ if (match) {
+ return `Email: ${match[1]}`;
+ }
+ }
+ return line;
+ })
+ .join("\n");
+ }
+
+ // Map the author information from sentByUser if available
+ const author = msg.sentByUser
+ ? {
+ name: msg.sentByUser.name,
+ email: msg.sentByUser.email,
+ type: (msg.sentByUser.isExternal ? "customer" : "user") as
+ | "user"
+ | "customer",
+ }
+ : undefined;
+
+ return {
+ id: msg.id,
+ content: content,
+ createdAt: msg.timestamp || msg.createdAt || "",
+ timestamp: msg.timestamp || msg.createdAt || "",
+ author: author,
+ };
+ });
+
+ conversation.messages = messages;
+ } else {
+ // Don't throw error, just leave messages empty
+ const errorText = await messagesResponse.text();
+ console.error("Failed to fetch messages:", errorText);
+ conversation.messages = [];
+ }
+
+ return conversation;
+}
+
+export async function createSupportTicket(
+ request: CreateSupportTicketRequest,
+): Promise {
+ if (!request.teamSlug) {
+ throw new Error("Team slug is required to create support ticket");
+ }
+
+ const token = await getAuthToken();
+ if (!token) {
+ throw new Error("No auth token available");
+ }
+
+ // Fetch wallet address (server-side)
+ const walletAddress = await getAuthTokenWalletAddress();
+
+ // URL encode the team slug to handle special characters like #
+ const encodedTeamSlug = encodeURIComponent(request.teamSlug);
+ const apiUrl = `${NEXT_PUBLIC_THIRDWEB_API_HOST}/v1/teams/${encodedTeamSlug}/support-conversations`;
+
+ // Build the payload for creating a conversation
+ // If the message does not already include wallet address, prepend it
+ let message = request.message;
+ if (!message.includes("Wallet address:")) {
+ message = `Wallet address: ${String(walletAddress || "-")}\n${message}`;
+ }
+
+ const payload = {
+ markdown: message.trim(),
+ title: request.title,
+ };
+
+ const body = JSON.stringify(payload);
+ const headers: Record = {
+ Accept: "application/json",
+ Authorization: `Bearer ${token}`,
+ "Content-Type": "application/json",
+ "Accept-Encoding": "identity",
+ };
+
+ const response = await fetch(apiUrl, {
+ body,
+ headers,
+ method: "POST",
+ });
+
+ if (!response.ok) {
+ const errorText = await response.text();
+ throw new Error(`API Server error: ${response.status} - ${errorText}`);
+ }
+
+ const createdConversation: SupportTicket = await response.json();
+
+ // Escalate to SIWA feedback endpoint if conversationId is provided
+ if (request.conversationId) {
+ try {
+ const siwaUrl = process.env.NEXT_PUBLIC_SIWA_URL;
+ if (siwaUrl) {
+ await fetch(`${siwaUrl}/v1/chat/feedback`, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ Authorization: `Bearer ${token}`,
+ ...(request.teamSlug ? { "x-team-id": request.teamSlug } : {}),
+ },
+ body: JSON.stringify({
+ conversationId: request.conversationId,
+ feedbackRating: 9999,
+ }),
+ });
+ }
+ } catch (error) {
+ // Log error but don't fail the ticket creation
+ console.error("Failed to escalate to SIWA feedback:", error);
+ }
+ }
+
+ return createdConversation;
+}
+
+export async function sendMessageToTicket(
+ request: SendMessageRequest,
+): Promise {
+ if (!request.ticketId || !request.teamSlug) {
+ throw new Error("Ticket ID and team slug are required");
+ }
+
+ const token = await getAuthToken();
+ if (!token) {
+ throw new Error("No auth token available");
+ }
+
+ // URL encode the team slug and ticket ID to handle special characters like #
+ const encodedTeamSlug = encodeURIComponent(request.teamSlug);
+ const encodedTicketId = encodeURIComponent(request.ticketId);
+ const apiUrl = `${NEXT_PUBLIC_THIRDWEB_API_HOST}/v1/teams/${encodedTeamSlug}/support-conversations/${encodedTicketId}/messages`;
+
+ // Build the payload for sending a message
+ // Append /unthread send for customer messages to ensure proper routing
+ const messageWithUnthread = `${request.message.trim()}\n/unthread send`;
+ const payload = {
+ markdown: messageWithUnthread,
+ };
+
+ const body = JSON.stringify(payload);
+ const headers: Record = {
+ Accept: "application/json",
+ Authorization: `Bearer ${token}`,
+ "Content-Type": "application/json",
+ "Accept-Encoding": "identity",
+ };
+
+ const response = await fetch(apiUrl, {
+ body,
+ headers,
+ method: "POST",
+ });
+
+ if (!response.ok) {
+ const errorText = await response.text();
+ throw new Error(`API Server error: ${response.status} - ${errorText}`);
+ }
+ // Message sent successfully, no need to return anything
+}
diff --git a/apps/dashboard/src/@/api/team.ts b/apps/dashboard/src/@/api/team.ts
index f8eeb3f43da..e30cf78095d 100644
--- a/apps/dashboard/src/@/api/team.ts
+++ b/apps/dashboard/src/@/api/team.ts
@@ -48,10 +48,6 @@ export async function service_getTeamBySlug(slug: string) {
return null;
}
-export function getTeamById(id: string) {
- return getTeamBySlug(id);
-}
-
export async function getTeams() {
const token = await getAuthToken();
if (!token) {
@@ -97,7 +93,7 @@ export async function getLastVisitedTeam() {
const lastVisitedTeamId = cookiesStore.get(LAST_USED_TEAM_ID)?.value;
if (lastVisitedTeamId) {
- const team = await getTeamById(lastVisitedTeamId);
+ const team = await getTeamBySlug(lastVisitedTeamId);
if (team) {
return team;
}
diff --git a/apps/dashboard/src/@/components/chat/CustomChatButton.tsx b/apps/dashboard/src/@/components/chat/CustomChatButton.tsx
index c6ebb0bf673..b6eb475792b 100644
--- a/apps/dashboard/src/@/components/chat/CustomChatButton.tsx
+++ b/apps/dashboard/src/@/components/chat/CustomChatButton.tsx
@@ -3,6 +3,7 @@
import { MessageCircleIcon, XIcon } from "lucide-react";
import { useCallback, useRef, useState } from "react";
import { createThirdwebClient } from "thirdweb";
+import type { Team } from "@/api/team";
import { Button } from "@/components/ui/button";
import { NEXT_PUBLIC_DASHBOARD_CLIENT_ID } from "@/constants/public-envs";
import { cn } from "@/lib/utils";
@@ -21,7 +22,7 @@ export function CustomChatButton(props: {
label: string;
examplePrompts: string[];
authToken: string | undefined;
- teamId: string | undefined;
+ team: Team; // changed from teamId
clientId: string | undefined;
requireLogin?: boolean;
}) {
@@ -82,7 +83,7 @@ export function CustomChatButton(props: {
}))}
networks={props.networks}
requireLogin={props.requireLogin}
- teamId={props.teamId}
+ team={props.team} // pass full team object
/>
)}
diff --git a/apps/dashboard/src/@/components/chat/CustomChatContent.tsx b/apps/dashboard/src/@/components/chat/CustomChatContent.tsx
index 6d46e6e0092..d0cb45d8964 100644
--- a/apps/dashboard/src/@/components/chat/CustomChatContent.tsx
+++ b/apps/dashboard/src/@/components/chat/CustomChatContent.tsx
@@ -5,6 +5,7 @@ import { usePathname } from "next/navigation";
import { useCallback, useState } from "react";
import type { ThirdwebClient } from "thirdweb";
import { useActiveWalletConnectionStatus } from "thirdweb/react";
+import type { Team } from "@/api/team";
import { Button } from "@/components/ui/button";
import { NebulaIcon } from "@/icons/NebulaIcon";
import { ChatBar } from "./ChatBar";
@@ -14,7 +15,7 @@ import type { ExamplePrompt, NebulaContext } from "./types";
export default function CustomChatContent(props: {
authToken: string | undefined;
- teamId: string | undefined;
+ team: Team;
clientId: string | undefined;
client: ThirdwebClient;
examplePrompts: ExamplePrompt[];
@@ -31,14 +32,14 @@ export default function CustomChatContent(props: {
client={props.client}
clientId={props.clientId}
examplePrompts={props.examplePrompts}
- teamId={props.teamId}
+ team={props.team}
/>
);
}
function CustomChatContentLoggedIn(props: {
authToken: string;
- teamId: string | undefined;
+ team: Team;
clientId: string | undefined;
client: ThirdwebClient;
examplePrompts: ExamplePrompt[];
@@ -55,6 +56,10 @@ function CustomChatContentLoggedIn(props: {
const [enableAutoScroll, setEnableAutoScroll] = useState(false);
const connectionStatus = useActiveWalletConnectionStatus();
+ // Support form state
+ const [showSupportForm, setShowSupportForm] = useState(false);
+ const [productLabel, setProductLabel] = useState("");
+
const handleSendMessage = useCallback(
async (userMessage: UserMessage) => {
const abortController = new AbortController();
@@ -96,7 +101,7 @@ function CustomChatContentLoggedIn(props: {
headers: {
Authorization: `Bearer ${props.authToken}`,
"Content-Type": "application/json",
- ...(props.teamId ? { "x-team-id": props.teamId } : {}),
+ ...(props.team.id ? { "x-team-id": props.team.id } : {}),
...(props.clientId ? { "x-client-id": props.clientId } : {}),
},
method: "POST",
@@ -132,7 +137,7 @@ function CustomChatContentLoggedIn(props: {
setEnableAutoScroll(false);
}
},
- [props.authToken, props.clientId, props.teamId, sessionId],
+ [props.authToken, props.clientId, props.team.id, sessionId],
);
const handleFeedback = useCallback(
@@ -165,7 +170,7 @@ function CustomChatContentLoggedIn(props: {
headers: {
Authorization: `Bearer ${props.authToken}`,
"Content-Type": "application/json",
- ...(props.teamId ? { "x-team-id": props.teamId } : {}),
+ ...(props.team.id ? { "x-team-id": props.team.id } : {}),
},
method: "POST",
});
@@ -188,7 +193,7 @@ function CustomChatContentLoggedIn(props: {
// Consider implementing retry logic here
}
},
- [sessionId, props.authToken, props.teamId, messages],
+ [sessionId, props.authToken, props.team.id, messages],
);
const showEmptyState = !userHasSubmittedMessage && messages.length === 0;
@@ -212,8 +217,14 @@ function CustomChatContentLoggedIn(props: {
sessionId={sessionId}
setEnableAutoScroll={setEnableAutoScroll}
useSmallText
+ showSupportForm={showSupportForm}
+ setShowSupportForm={setShowSupportForm}
+ productLabel={productLabel}
+ setProductLabel={setProductLabel}
+ team={props.team}
/>
)}
+ {/* Removed floating support case button and form */}
{
chatAbortController?.abort();
diff --git a/apps/dashboard/src/@/components/chat/CustomChats.tsx b/apps/dashboard/src/@/components/chat/CustomChats.tsx
index d3953f18f7b..65018b471b5 100644
--- a/apps/dashboard/src/@/components/chat/CustomChats.tsx
+++ b/apps/dashboard/src/@/components/chat/CustomChats.tsx
@@ -1,14 +1,18 @@
import {
AlertCircleIcon,
+ ArrowRightIcon,
MessageCircleIcon,
ThumbsDownIcon,
ThumbsUpIcon,
} from "lucide-react";
import { useEffect, useRef, useState } from "react";
import type { ThirdwebClient } from "thirdweb";
+import type { Team } from "@/api/team";
import { MarkdownRenderer } from "@/components/blocks/markdown-renderer";
+import { Button } from "@/components/ui/button";
import { ScrollShadow } from "@/components/ui/ScrollShadow/ScrollShadow";
import { cn } from "@/lib/utils";
+import { SupportTicketForm } from "../../../app/(app)/team/[team_slug]/(team)/~/support/_components/SupportTicketForm";
import { Reasoning } from "./Reasoning";
// Define local types
@@ -47,10 +51,18 @@ export function CustomChats(props: {
useSmallText?: boolean;
sendMessage: (message: UserMessage) => void;
onFeedback?: (messageIndex: number, feedback: 1 | -1) => void;
+ // New props for support form
+ showSupportForm: boolean;
+ setShowSupportForm: (v: boolean) => void;
+ productLabel: string;
+ setProductLabel: (v: string) => void;
+ team: Team;
}) {
const { messages, setEnableAutoScroll, enableAutoScroll } = props;
const scrollAnchorRef = useRef(null);
const chatContainerRef = useRef(null);
+ // Add state to track if a support ticket was created
+ const [supportTicketCreated, setSupportTicketCreated] = useState(false);
// auto scroll to bottom when messages change
// eslint-disable-next-line no-restricted-syntax
@@ -123,6 +135,68 @@ export function CustomChats(props: {
sendMessage={props.sendMessage}
sessionId={props.sessionId}
/>
+ {/* Support Case Button/Form in last assistant message */}
+ {message.type === "assistant" &&
+ index === props.messages.length - 1 && (
+ <>
+ {/* Only show button/form if ticket not created */}
+ {!props.showSupportForm && !supportTicketCreated && (
+
+
props.setShowSupportForm(true)}
+ size="sm"
+ className="bg-primary hover:bg-primary/80 text-primary-foreground transition-opacity"
+ >
+ Create Support Case
+
+
+
+ )}
+ {/* Show form if open and ticket not created */}
+ {props.showSupportForm && !supportTicketCreated && (
+
+ {
+ props.setShowSupportForm(false);
+ props.setProductLabel("");
+ setSupportTicketCreated(true);
+ }}
+ />
+
+ )}
+ {/* Show success message if ticket created */}
+ {supportTicketCreated && (
+
+
+ Your support ticket has been created! Our team
+ will get back to you soon.
+
+
+
+ )}
+ >
+ )}
);
})}
diff --git a/apps/dashboard/src/@/lib/support-utils.ts b/apps/dashboard/src/@/lib/support-utils.ts
new file mode 100644
index 00000000000..cbe62bbe04f
--- /dev/null
+++ b/apps/dashboard/src/@/lib/support-utils.ts
@@ -0,0 +1,44 @@
+/**
+ * Shared utility functions for support ticket status handling
+ */
+
+/**
+ * Get the CSS classes for a support ticket status badge
+ */
+export function getStatusColor(status: string): string {
+ switch (status.toLowerCase()) {
+ case "resolved":
+ case "closed":
+ return "border-muted text-muted-foreground bg-muted/10";
+ case "in_progress":
+ return "border-destructive text-destructive bg-destructive/10";
+ case "needs_response":
+ return "warning";
+ case "on_hold":
+ return "border-secondary text-secondary-foreground bg-secondary/10";
+ default:
+ return "warning";
+ }
+}
+
+/**
+ * Get the display label for a support ticket status
+ */
+export function getStatusLabel(status: string): string {
+ const statusLower = status.toLowerCase();
+
+ switch (statusLower) {
+ case "closed":
+ return "Closed";
+ case "resolved":
+ return "Resolved";
+ case "in_progress":
+ return "Needs Response";
+ case "needs_response":
+ return "In Progress";
+ case "on_hold":
+ return "On Hold";
+ default:
+ return "In Progress";
+ }
+}
diff --git a/apps/dashboard/src/app/(app)/(dashboard)/support/create-ticket/components/contact-forms/account/index.tsx b/apps/dashboard/src/app/(app)/(dashboard)/support/create-ticket/components/contact-forms/account/index.tsx
deleted file mode 100644
index 28d52421d3e..00000000000
--- a/apps/dashboard/src/app/(app)/(dashboard)/support/create-ticket/components/contact-forms/account/index.tsx
+++ /dev/null
@@ -1,67 +0,0 @@
-import { type ReactElement, useState } from "react";
-import { AttachmentForm } from "../shared/SupportForm_AttachmentUploader";
-import { DescriptionInput } from "../shared/SupportForm_DescriptionInput";
-import { SupportForm_SelectInput } from "../shared/SupportForm_SelectInput";
-
-type ProblemAreaItem = {
- label: string;
- component: ReactElement;
-};
-
-const ACCOUNT_PROBLEM_AREAS: ProblemAreaItem[] = [
- {
- component: (
- <>
-
-
- >
- ),
- label: "Pricing inquiry",
- },
- {
- component: (
- <>
-
-
- >
- ),
- label: "Billing inquiry",
- },
- {
- component: (
- <>
-
-
- >
- ),
- label: "Usage inquiry",
- },
- {
- component: (
- <>
-
-
- >
- ),
- label: "Other",
- },
-];
-
-export default function AccountSupportForm() {
- const [problemArea, setProblemArea] = useState("");
-
- return (
- <>
- o.label)}
- promptText="Select a problem area"
- required={true}
- value={problemArea}
- />
- {ACCOUNT_PROBLEM_AREAS.find((o) => o.label === problemArea)?.component}
- >
- );
-}
diff --git a/apps/dashboard/src/app/(app)/(dashboard)/support/create-ticket/components/contact-forms/other/index.tsx b/apps/dashboard/src/app/(app)/(dashboard)/support/create-ticket/components/contact-forms/other/index.tsx
deleted file mode 100644
index fcbc0440670..00000000000
--- a/apps/dashboard/src/app/(app)/(dashboard)/support/create-ticket/components/contact-forms/other/index.tsx
+++ /dev/null
@@ -1,66 +0,0 @@
-import { type ReactElement, useState } from "react";
-import { AttachmentForm } from "../shared/SupportForm_AttachmentUploader";
-import { DescriptionInput } from "../shared/SupportForm_DescriptionInput";
-import { SupportForm_SelectInput } from "../shared/SupportForm_SelectInput";
-
-type ProblemAreaItem = {
- label: string;
- component: ReactElement;
-};
-
-const OTHER_PROBLEM_AREAS: ProblemAreaItem[] = [
- {
- component: (
- <>
-
-
- >
- ),
- label: "General inquiry",
- },
- {
- component: (
- <>
-
-
- >
- ),
- label: "Security",
- },
- {
- component: (
- <>
-
-
- >
- ),
- label: "Feedback",
- },
- {
- component: (
- <>
-
-
- >
- ),
- label: "Other",
- },
-];
-
-export default function OtherSupportForm() {
- const [problemArea, setProblemArea] = useState("");
- return (
- <>
- o.label)}
- promptText="Select a problem area"
- required={true}
- value={problemArea}
- />
- {OTHER_PROBLEM_AREAS.find((o) => o.label === problemArea)?.component}
- >
- );
-}
diff --git a/apps/dashboard/src/app/(app)/(dashboard)/support/create-ticket/components/contact-forms/shared/SupportForm_AttachmentUploader.tsx b/apps/dashboard/src/app/(app)/(dashboard)/support/create-ticket/components/contact-forms/shared/SupportForm_AttachmentUploader.tsx
deleted file mode 100644
index 9cd3479f633..00000000000
--- a/apps/dashboard/src/app/(app)/(dashboard)/support/create-ticket/components/contact-forms/shared/SupportForm_AttachmentUploader.tsx
+++ /dev/null
@@ -1,17 +0,0 @@
-import { Label } from "@/components/ui/label";
-
-export const AttachmentForm = () => {
- return (
-
-
- Attachments
-
-
-
- );
-};
diff --git a/apps/dashboard/src/app/(app)/(dashboard)/support/create-ticket/components/contact-forms/shared/SupportForm_TeamSelection.tsx b/apps/dashboard/src/app/(app)/(dashboard)/support/create-ticket/components/contact-forms/shared/SupportForm_TeamSelection.tsx
deleted file mode 100644
index 32db19a1465..00000000000
--- a/apps/dashboard/src/app/(app)/(dashboard)/support/create-ticket/components/contact-forms/shared/SupportForm_TeamSelection.tsx
+++ /dev/null
@@ -1,58 +0,0 @@
-import { useId } from "react";
-import { Label } from "@/components/ui/label";
-import {
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue,
-} from "@/components/ui/select";
-
-type MinimalTeam = {
- name: string;
- id: string;
-};
-
-type Props = {
- teams: MinimalTeam[];
- selectedTeamId: string | undefined;
- onChange: (teamId: string) => void;
-};
-
-export const SupportForm_TeamSelection = (props: Props) => {
- const selectedTeamName = props.teams.find(
- (t) => t.id === props.selectedTeamId,
- )?.name;
-
- const teamId = useId();
-
- return (
-
-
- Select Team
- •
-
-
- {
- props.onChange(selectedId);
- }}
- value={props.selectedTeamId}
- >
-
-
- {selectedTeamName}
-
-
-
- {props.teams.map((team) => (
-
- {team.name}
-
- ))}
-
-
-
- );
-};
diff --git a/apps/dashboard/src/app/(app)/(dashboard)/support/create-ticket/components/contact-forms/shared/SupportForm_TelegramInput.tsx b/apps/dashboard/src/app/(app)/(dashboard)/support/create-ticket/components/contact-forms/shared/SupportForm_TelegramInput.tsx
deleted file mode 100644
index fc79ad23bd3..00000000000
--- a/apps/dashboard/src/app/(app)/(dashboard)/support/create-ticket/components/contact-forms/shared/SupportForm_TelegramInput.tsx
+++ /dev/null
@@ -1,27 +0,0 @@
-import { Label } from "@/components/ui/label";
-
-type Props = {
- placeholder?: string;
-};
-
-const defaultDescription = "@YourHandle";
-
-export const SupportForm_TelegramInput = (props: Props) => {
- return (
-
-
- Telegram
- •
-
-
-
-
- );
-};
diff --git a/apps/dashboard/src/app/(app)/(dashboard)/support/create-ticket/components/create-ticket.action.ts b/apps/dashboard/src/app/(app)/(dashboard)/support/create-ticket/components/create-ticket.action.ts
deleted file mode 100644
index 60c6b7b17e9..00000000000
--- a/apps/dashboard/src/app/(app)/(dashboard)/support/create-ticket/components/create-ticket.action.ts
+++ /dev/null
@@ -1,208 +0,0 @@
-"use server";
-import "server-only";
-
-import { getAuthTokenWalletAddress } from "@/api/auth-token";
-import { getTeamById } from "@/api/team";
-import { loginRedirect } from "@/utils/redirects";
-import { getRawAccount } from "../../../../account/settings/getAccount";
-
-type State = {
- success: boolean;
- message: string;
-};
-
-const UNTHREAD_API_KEY = process.env.UNTHREAD_API_KEY || "";
-
-const planToCustomerId = {
- accelerate: process.env.UNTHREAD_ACCELERATE_TIER_ID as string,
- free: process.env.UNTHREAD_FREE_TIER_ID as string,
- growth: process.env.UNTHREAD_GROWTH_TIER_ID as string,
- pro: process.env.UNTHREAD_PRO_TIER_ID as string,
- scale: process.env.UNTHREAD_SCALE_TIER_ID as string,
- // treat starter as free
- starter: process.env.UNTHREAD_FREE_TIER_ID as string,
-} as const;
-
-function isValidPlan(plan: string): plan is keyof typeof planToCustomerId {
- return plan in planToCustomerId;
-}
-
-function prepareEmailTitle(
- product: string,
- problemArea: string,
- email: string,
- name: string,
-) {
- const title =
- product && problemArea
- ? `${product}: ${problemArea} (${email})`
- : `New ticket from ${name} (${email})`;
- return title;
-}
-
-function prepareEmailBody(props: {
- product: string;
- markdownInput: string;
- email: string;
- name: string;
- extraInfoInput: Record;
- walletAddress: string;
- telegramHandle: string;
-}) {
- const {
- extraInfoInput,
- email,
- walletAddress,
- product,
- name,
- markdownInput,
- telegramHandle,
- } = props;
- // Update `markdown` to include the infos from the form
- const extraInfo = Object.keys(extraInfoInput)
- .filter((key) => key.startsWith("extraInfo_"))
- .map((key) => {
- const prettifiedKey = `# ${key
- .replace("extraInfo_", "")
- .replaceAll("_", " ")}`;
- return `${prettifiedKey}: ${extraInfoInput[key] ?? "N/A"}\n`;
- })
- .join("");
- const markdown = `# Email: ${email}
- # Name: ${name}
- # Telegram: ${telegramHandle}
- # Wallet address: ${walletAddress}
- # Product: ${product}
- ${extraInfo}
- # Message:
- ${markdownInput}
- `;
- return markdown;
-}
-
-export async function createTicketAction(
- _previousState: State,
- formData: FormData,
-) {
- const teamId = formData.get("teamId")?.toString();
-
- if (!teamId) {
- return {
- message: "teamId is required",
- success: false,
- };
- }
-
- const team = await getTeamById(teamId);
-
- if (!team) {
- return {
- message: `Team with id "${teamId}" not found`,
- success: false,
- };
- }
-
- const [walletAddress, account] = await Promise.all([
- getAuthTokenWalletAddress(),
- getRawAccount(),
- ]);
-
- if (!walletAddress || !account) {
- loginRedirect("/support");
- }
-
- const customerId = isValidPlan(team.supportPlan)
- ? // fall back to "free" tier
- planToCustomerId[team.supportPlan] || planToCustomerId.free
- : // fallback to "free" tier
- planToCustomerId.free;
-
- const product = formData.get("product")?.toString() || "";
- const problemArea = formData.get("extraInfo_Problem_Area")?.toString() || "";
- const telegramHandle = formData.get("telegram")?.toString() || "";
-
- const title = prepareEmailTitle(
- product,
- problemArea,
- account.email || "",
- account.name || "",
- );
-
- const keyVal: Record = {};
- for (const key of formData.keys()) {
- keyVal[key] = formData.get(key)?.toString() || "";
- }
-
- const markdown = prepareEmailBody({
- email: account.email || "",
- extraInfoInput: keyVal,
- markdownInput: keyVal.markdown || "",
- name: account.name || "",
- product,
- telegramHandle: telegramHandle,
- walletAddress: walletAddress,
- });
-
- const content = {
- customerId,
- emailInboxId: process.env.UNTHREAD_EMAIL_INBOX_ID,
- markdown,
- onBehalfOf: {
- email: account.email,
- id: account.id,
- name: account.name,
- },
- status: "open",
- title,
- triageChannelId: process.env.UNTHREAD_TRIAGE_CHANNEL_ID,
- type: "email",
- };
-
- // check files
- const files = formData.getAll("files") as File[];
-
- if (files.length > 10) {
- return { message: "You can only attach 10 files at once.", success: false };
- }
- if (files.some((file) => file.size > 10 * 1024 * 1024)) {
- return { message: "The max file size is 20MB.", success: false };
- }
-
- // add the content
- formData.append("json", JSON.stringify(content));
-
- const KEEP_FIELDS = ["attachments", "json"];
- const keys = [...formData.keys()];
- // delete everything except attachments off of the form data
- for (const key of keys) {
- if (!KEEP_FIELDS.includes(key)) {
- formData.delete(key);
- }
- }
-
- // actually create the ticket
- const res = await fetch("https://api.unthread.io/api/conversations", {
- body: formData,
- headers: {
- "X-Api-Key": UNTHREAD_API_KEY,
- },
- method: "POST",
- });
- if (!res.ok) {
- console.error(
- "Failed to create ticket",
- res.status,
- res.statusText,
- await res.text(),
- );
- return {
- message: "Failed to create ticket, please try again later.",
- success: false,
- };
- }
-
- return {
- message: "Ticket created successfully",
- success: true,
- };
-}
diff --git a/apps/dashboard/src/app/(app)/(dashboard)/support/create-ticket/components/create-ticket.client.tsx b/apps/dashboard/src/app/(app)/(dashboard)/support/create-ticket/components/create-ticket.client.tsx
deleted file mode 100644
index 6b34630770f..00000000000
--- a/apps/dashboard/src/app/(app)/(dashboard)/support/create-ticket/components/create-ticket.client.tsx
+++ /dev/null
@@ -1,183 +0,0 @@
-"use client";
-
-import { SupportForm_SelectInput } from "@app/(dashboard)/support/create-ticket/components/contact-forms/shared/SupportForm_SelectInput";
-import { SupportForm_TeamSelection } from "@app/(dashboard)/support/create-ticket/components/contact-forms/shared/SupportForm_TeamSelection";
-import { SupportForm_TelegramInput } from "@app/(dashboard)/support/create-ticket/components/contact-forms/shared/SupportForm_TelegramInput";
-import dynamic from "next/dynamic";
-import {
- type ReactElement,
- useActionState,
- useEffect,
- useRef,
- useState,
-} from "react";
-import { useFormStatus } from "react-dom";
-import { toast } from "sonner";
-import { Button } from "@/components/ui/button";
-import { Spinner } from "@/components/ui/Spinner/Spinner";
-import { Skeleton } from "@/components/ui/skeleton";
-import { cn } from "@/lib/utils";
-import { createTicketAction } from "./create-ticket.action";
-
-const ConnectSupportForm = dynamic(() => import("./contact-forms/connect"), {
- loading: () => ,
- ssr: false,
-});
-const EngineSupportForm = dynamic(() => import("./contact-forms/engine"), {
- loading: () => ,
- ssr: false,
-});
-const ContractSupportForm = dynamic(() => import("./contact-forms/contracts"), {
- loading: () => ,
- ssr: false,
-});
-const AccountSupportForm = dynamic(() => import("./contact-forms/account"), {
- loading: () => ,
- ssr: false,
-});
-const OtherSupportForm = dynamic(() => import("./contact-forms/other"), {
- loading: () => ,
- ssr: false,
-});
-
-const productOptions: { label: string; component: ReactElement }[] = [
- {
- component: ,
- label: "Connect",
- },
- {
- component: ,
- label: "Engine",
- },
- {
- component: ,
- label: "Contracts",
- },
- {
- component: ,
- label: "Account",
- },
- {
- component: ,
- label: "Other",
- },
-];
-
-function ProductAreaSelection(props: {
- productLabel: string;
- setProductLabel: (val: string) => void;
-}) {
- const { productLabel, setProductLabel } = props;
-
- return (
-
- o.label)}
- promptText="Select a product"
- required={true}
- value={productLabel}
- />
- {productOptions.find((o) => o.label === productLabel)?.component}
-
- );
-}
-
-export function CreateTicket(props: {
- teams: {
- name: string;
- id: string;
- }[];
-}) {
- const formRef = useRef(null);
- const [selectedTeamId, setSelectedTeamId] = useState(
- props.teams[0]?.id,
- );
-
- const [productLabel, setProductLabel] = useState("");
-
- const [state, formAction] = useActionState(createTicketAction, {
- message: "",
- success: false,
- });
-
- // needed here
- // eslint-disable-next-line no-restricted-syntax
- useEffect(() => {
- if (!state.message) {
- return;
- }
- if (state.success) {
- toast.success(state.message);
- } else {
- toast.error(state.message);
- }
- }, [state.success, state.message]);
-
- return (
-
- );
-}
-
-function SubmitButton() {
- const { pending } = useFormStatus();
- return (
-
- {pending && }
- {pending ? "Submitting" : "Submit"}
-
- );
-}
diff --git a/apps/dashboard/src/app/(app)/(dashboard)/support/create-ticket/page.tsx b/apps/dashboard/src/app/(app)/(dashboard)/support/create-ticket/page.tsx
deleted file mode 100644
index a4e9cc9751a..00000000000
--- a/apps/dashboard/src/app/(app)/(dashboard)/support/create-ticket/page.tsx
+++ /dev/null
@@ -1,48 +0,0 @@
-import Link from "next/link";
-import { getTeams } from "@/api/team";
-import {
- Breadcrumb,
- BreadcrumbItem,
- BreadcrumbLink,
- BreadcrumbList,
- BreadcrumbPage,
- BreadcrumbSeparator,
-} from "@/components/ui/breadcrumb";
-import { loginRedirect } from "@/utils/redirects";
-import { CreateTicket } from "./components/create-ticket.client";
-
-export default async function Page() {
- const teams = await getTeams();
-
- const pagePath = "/support/create-ticket";
-
- if (!teams || teams.length === 0) {
- loginRedirect(pagePath);
- }
-
- return (
-
-
-
-
-
- Support
-
-
-
-
- Get Support
-
-
-
-
- ({
- id: t.id,
- name: t.name,
- }))}
- />
-
-
- );
-}
diff --git a/apps/dashboard/src/app/(app)/(dashboard)/support/opengraph-image.png b/apps/dashboard/src/app/(app)/(dashboard)/support/opengraph-image.png
deleted file mode 100644
index b41aca75a60..00000000000
Binary files a/apps/dashboard/src/app/(app)/(dashboard)/support/opengraph-image.png and /dev/null differ
diff --git a/apps/dashboard/src/app/(app)/(dashboard)/support/page.tsx b/apps/dashboard/src/app/(app)/(dashboard)/support/page.tsx
index bce10cb8765..c3f8e8be640 100644
--- a/apps/dashboard/src/app/(app)/(dashboard)/support/page.tsx
+++ b/apps/dashboard/src/app/(app)/(dashboard)/support/page.tsx
@@ -71,7 +71,7 @@ export default async function SupportPage() {
]);
const teams = await getTeams();
- const teamId = teams?.[0]?.id ?? undefined;
+ const team = teams?.[0];
return (
@@ -92,21 +92,25 @@ export default async function SupportPage() {
team.
-
+ {team && (
+
+ )}
diff --git a/apps/dashboard/src/app/(app)/components/Header/SecondaryNav/SecondaryNav.tsx b/apps/dashboard/src/app/(app)/components/Header/SecondaryNav/SecondaryNav.tsx
index 3f505aa1602..4a0b12adccf 100644
--- a/apps/dashboard/src/app/(app)/components/Header/SecondaryNav/SecondaryNav.tsx
+++ b/apps/dashboard/src/app/(app)/components/Header/SecondaryNav/SecondaryNav.tsx
@@ -46,7 +46,7 @@ export function SecondaryNavLinks() {
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/TeamSidebarLayout.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/TeamSidebarLayout.tsx
index d28df97442b..e83543e8844 100644
--- a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/TeamSidebarLayout.tsx
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/TeamSidebarLayout.tsx
@@ -6,6 +6,7 @@ import {
DatabaseIcon,
DollarSignIcon,
FileTextIcon,
+ HelpCircleIcon,
HomeIcon,
SettingsIcon,
WalletCardsIcon,
@@ -89,6 +90,11 @@ export function TeamSidebarLayout(props: {
: []),
]}
footerSidebarLinks={[
+ {
+ href: `${layoutPath}/~/support`,
+ icon: HelpCircleIcon,
+ label: "Support",
+ },
{
href: `${layoutPath}/~/billing`,
icon: DollarSignIcon,
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/layout.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/layout.tsx
index 02cf04df252..4e4f3574827 100644
--- a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/layout.tsx
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/layout.tsx
@@ -103,19 +103,21 @@ export default async function TeamLayout(props: {
>
{props.children}
-
-
-
+ {team && (
+
+
+
+ )}
);
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/SupportLayout.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/SupportLayout.tsx
new file mode 100644
index 00000000000..ddbf5ecaddf
--- /dev/null
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/SupportLayout.tsx
@@ -0,0 +1,19 @@
+"use client";
+
+import { cn } from "@/lib/utils";
+
+interface SupportLayoutProps {
+ children: React.ReactNode;
+ className?: string;
+}
+
+export function SupportLayout(props: SupportLayoutProps): React.ReactElement {
+ return (
+
+ {/* Page content */}
+
+
+ );
+}
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/CreateSupportCase.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/CreateSupportCase.tsx
new file mode 100644
index 00000000000..5422aca324e
--- /dev/null
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/CreateSupportCase.tsx
@@ -0,0 +1,392 @@
+"use client";
+
+import { ArrowLeftIcon, BotIcon, SendIcon } from "lucide-react";
+import { useCallback, useEffect, useRef, useState } from "react";
+import type { Team } from "@/api/team";
+import { MarkdownRenderer } from "@/components/blocks/markdown-renderer";
+import { Reasoning } from "@/components/chat/Reasoning";
+import { Button } from "@/components/ui/button";
+import { Textarea } from "@/components/ui/textarea";
+import { useDashboardRouter } from "@/lib/DashboardRouter";
+import { SupportHeader } from "./SupportHeader";
+import { SupportTicketForm } from "./SupportTicketForm";
+
+interface CreateSupportCaseProps {
+ team: Team;
+ authToken?: string;
+}
+
+export function CreateSupportCase({ team, authToken }: CreateSupportCaseProps) {
+ const [chatMessages, setChatMessages] = useState<
+ {
+ id: number;
+ content: string;
+ isUser: boolean;
+ timestamp: string;
+ isSuccessMessage?: boolean;
+ }[]
+ >([]);
+ const [chatInput, setChatInput] = useState("");
+ const [conversationId, setConversationId] = useState(
+ undefined,
+ );
+ const chatContainerRef = useRef(null);
+ const messagesEndRef = useRef(null);
+ const router = useDashboardRouter();
+
+ // Form states
+ const [showCreateForm, setShowCreateForm] = useState(false);
+ const [productLabel, setProductLabel] = useState("");
+
+ // Auto scroll to bottom when AI chat messages change
+ // eslint-disable-next-line no-restricted-syntax
+ useEffect(() => {
+ if (chatContainerRef.current && chatMessages.length > 0) {
+ chatContainerRef.current.scrollTop =
+ chatContainerRef.current.scrollHeight;
+ }
+ }, [chatMessages]);
+
+ const handleBackToSupport = () => {
+ router.push(`/team/${team.slug}/~/support`);
+ };
+
+ // Extracted sendMessageToSiwa function to avoid duplication
+ const sendMessageToSiwa = useCallback(
+ async (message: string, currentConversationId?: string) => {
+ if (!authToken) {
+ throw new Error("Authentication token is required");
+ }
+
+ const apiUrl = process.env.NEXT_PUBLIC_SIWA_URL;
+ const payload = {
+ conversationId: currentConversationId,
+ message,
+ source: "support-in-dashboard",
+ };
+ const response = await fetch(`${apiUrl}/v1/chat`, {
+ body: JSON.stringify(payload),
+ headers: {
+ Authorization: `Bearer ${authToken}`,
+ "Content-Type": "application/json",
+ "x-team-id": team.id,
+ },
+ method: "POST",
+ });
+ const data = await response.json();
+ if (
+ data.conversationId &&
+ data.conversationId !== currentConversationId
+ ) {
+ setConversationId(data.conversationId);
+ }
+ return data.data;
+ },
+ [authToken, team.id],
+ );
+
+ const _handleStartChat = useCallback(
+ async (initialMessage?: string) => {
+ // Show initial AI greeting message immediately
+ const initialAIMsg = {
+ content:
+ "Hi! I'm thirdweb's AI assistant. I can help you troubleshoot issues, answer questions about our products, or create a support case for you. What can I help you with today?",
+ id: Date.now(),
+ isUser: false,
+ timestamp: new Date().toISOString(),
+ };
+
+ setChatMessages([initialAIMsg]);
+ setChatInput("");
+
+ // If there's an initial message from the user, add it and get AI response
+ if (initialMessage) {
+ const userMsg = {
+ content: initialMessage,
+ id: Date.now() + 1,
+ isUser: true,
+ timestamp: new Date().toISOString(),
+ };
+
+ setChatMessages([
+ initialAIMsg,
+ userMsg,
+ {
+ content: "__reasoning__",
+ id: Date.now() + 2,
+ isUser: false,
+ timestamp: new Date().toISOString(),
+ },
+ ]);
+
+ try {
+ const aiResponse = await sendMessageToSiwa(initialMessage);
+ setChatMessages([
+ initialAIMsg,
+ userMsg,
+ {
+ content: aiResponse,
+ id: Date.now() + 3,
+ isUser: false,
+ timestamp: new Date().toISOString(),
+ },
+ ]);
+ } catch (error) {
+ console.error("Error in handleStartChat:", error);
+ setChatMessages([
+ initialAIMsg,
+ userMsg,
+ {
+ content: "Sorry, something went wrong. Please try again.",
+ id: Date.now() + 4,
+ isUser: false,
+ timestamp: new Date().toISOString(),
+ },
+ ]);
+ }
+ }
+ },
+ [sendMessageToSiwa],
+ );
+
+ const handleChatSend = useCallback(async () => {
+ if (!chatInput.trim()) return;
+
+ const currentInput = chatInput;
+ setChatInput("");
+
+ const userMsg = {
+ content: currentInput,
+ id: Date.now(),
+ isUser: true,
+ timestamp: new Date().toISOString(),
+ };
+
+ const loadingMsg = {
+ content: "__reasoning__",
+ id: Date.now() + 1,
+ isUser: false,
+ timestamp: new Date().toISOString(),
+ };
+
+ setChatMessages((msgs) => [...msgs, userMsg, loadingMsg]);
+
+ try {
+ const aiResponse = await sendMessageToSiwa(currentInput, conversationId);
+ setChatMessages((msgs) => [
+ ...msgs.slice(0, -1), // remove loading
+ {
+ content: aiResponse,
+ id: Date.now() + 2,
+ isUser: false,
+ timestamp: new Date().toISOString(),
+ },
+ ]);
+ } catch (error) {
+ console.error("Error in handleChatSend:", error);
+ setChatMessages((msgs) => [
+ ...msgs.slice(0, -1),
+ {
+ content: "Sorry, something went wrong. Please try again.",
+ id: Date.now() + 3,
+ isUser: false,
+ timestamp: new Date().toISOString(),
+ },
+ ]);
+ }
+ }, [chatInput, conversationId, sendMessageToSiwa]);
+
+ const handleChatKeyPress = useCallback(
+ (e: React.KeyboardEvent) => {
+ if (e.key === "Enter" && !e.shiftKey) {
+ e.preventDefault();
+ handleChatSend();
+ }
+ },
+ [handleChatSend],
+ );
+
+ // Start the chat when component mounts
+ // eslint-disable-next-line no-restricted-syntax
+ useEffect(() => {
+ _handleStartChat();
+ }, [_handleStartChat]);
+
+ return (
+
+
{}}
+ />
+
+
+
+
+
+
+ Back to Support
+
+
+
+
+
+
+ {/* Chat Header */}
+
+
+
+
+ Ask AI for support
+
+
Online
+
+
+
+ {/* Chat Messages */}
+
+ {chatMessages.map((message, index) => (
+
+
+ {message.isUser ? (
+
+ {message.content}
+
+ ) : message.content === "__reasoning__" ? (
+
+ ) : (
+
+ )}
+
+ {new Date(message.timestamp).toLocaleTimeString()}
+
+
+ {/* Show Back to Support button for success message */}
+ {!message.isUser && message.isSuccessMessage && (
+
+
+ Back to Support
+
+
+ )}
+
+ {/* Show Create Support Case button in the AI response - only if form not shown and after user interaction */}
+ {!message.isUser &&
+ index === chatMessages.length - 1 &&
+ !showCreateForm &&
+ !message.isSuccessMessage &&
+ message.content !== "__reasoning__" &&
+ chatMessages.length > 2 && (
+
+ setShowCreateForm(true)}
+ size="sm"
+ className="bg-primary hover:bg-primary/80 text-primary-foreground transition-opacity"
+ >
+ Create Support Case
+
+
+ )}
+
+ {/* Show Support Case Form in the same message bubble when button is clicked */}
+ {!message.isUser &&
+ index === chatMessages.length - 1 &&
+ showCreateForm &&
+ message.content !== "__reasoning__" && (
+
+
+
+ Create Support Case
+
+
+ Let's create a detailed support case for our
+ technical team.
+
+
+
+
{
+ setShowCreateForm(false);
+ setProductLabel("");
+ setChatMessages((prev) => [
+ ...prev,
+ {
+ id: Date.now(),
+ content: `✅ **Support case created successfully!**\n\nYour case has been submitted to our technical team. You'll receive updates via email at ${team.billingEmail}.\n\nYou can track your case in the support portal above.`,
+ isUser: false,
+ timestamp: new Date().toISOString(),
+ isSuccessMessage: true,
+ },
+ ]);
+ }}
+ />
+
+ )}
+
+
+ ))}
+
+
+
+ {/* Chat Input */}
+ {!showCreateForm && (
+
+
+
+
+ Press Enter to send, Shift+Enter for new line
+
+
+ )}
+
+
+
+
+ );
+}
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/SupportCaseDetails.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/SupportCaseDetails.tsx
new file mode 100644
index 00000000000..1a2069050a5
--- /dev/null
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/SupportCaseDetails.tsx
@@ -0,0 +1,250 @@
+"use client";
+
+import { format } from "date-fns";
+import { ArrowLeftIcon, SendIcon } from "lucide-react";
+import { useRef, useState } from "react";
+import { toast } from "sonner";
+import { revalidatePathAction } from "@/actions/revalidate";
+import type { SupportTicket } from "@/api/support";
+import { sendMessageToTicket } from "@/api/support";
+import type { Team } from "@/api/team";
+import { Badge } from "@/components/ui/badge";
+import { Button } from "@/components/ui/button";
+import { Textarea } from "@/components/ui/textarea";
+import { useDashboardRouter } from "@/lib/DashboardRouter";
+import { getStatusColor, getStatusLabel } from "@/lib/support-utils";
+import { SupportHeader } from "./SupportHeader";
+
+interface SupportCaseDetailsProps {
+ ticket: SupportTicket;
+ team: Team;
+}
+
+export function SupportCaseDetails({ ticket, team }: SupportCaseDetailsProps) {
+ const [replyMessage, setReplyMessage] = useState("");
+ const [isSubmittingReply, setIsSubmittingReply] = useState(false);
+ const messagesEndRef = useRef(null);
+ const replySectionRef = useRef(null);
+ const router = useDashboardRouter();
+
+ const handleBackToCases = () => {
+ router.push(`/team/${team.slug}/~/support`);
+ };
+
+ const handleSendReply = async () => {
+ if (
+ !ticket ||
+ !replyMessage.trim() ||
+ !team.unthreadCustomerId ||
+ !team.billingEmail
+ ) {
+ toast.error("Please enter a message");
+ return;
+ }
+
+ setIsSubmittingReply(true);
+
+ try {
+ await sendMessageToTicket({
+ message: replyMessage,
+ teamSlug: team.slug,
+ ticketId: ticket.id,
+ });
+
+ toast.success("Reply sent successfully!");
+ setReplyMessage("");
+
+ // Revalidate the cache to fetch the latest messages
+ await revalidatePathAction(
+ `/team/${team.slug}/~/support/cases/${ticket.id}`,
+ "page",
+ );
+
+ // Refresh the page to show the new message
+ router.refresh();
+ } catch (error) {
+ console.error("Failed to send reply:", error);
+ toast.error("Failed to send reply. Please try again.");
+ } finally {
+ setIsSubmittingReply(false);
+ }
+ };
+
+ const handleKeyDown = (e: React.KeyboardEvent) => {
+ if ((e.metaKey || e.ctrlKey) && e.key === "Enter") {
+ e.preventDefault();
+ handleSendReply();
+ }
+ };
+
+ return (
+
+
{}}
+ />
+
+
+
+
+ ← Back to Cases
+
+
+
+
+
+
+ Ticket #{ticket.id}
+
+
+
+ {getStatusLabel(ticket.status)}
+
+
+
+
+
+
+
+
Created:
+
+ {format(new Date(ticket.createdAt), "MMM d, yyyy")}
+
+
+
+
+
+
+ Messages
+
+
+ {ticket.messages && ticket.messages.length > 0 ? (
+ ticket.messages.map((message) => {
+ const isCustomer = message.author?.type === "customer";
+ const authorName = message.author?.name || "Support";
+ const displayName = isCustomer ? "You" : authorName;
+
+ let messageContent =
+ message.content || "No content available";
+
+ if (messageContent.includes("/unthread send")) {
+ messageContent = messageContent
+ .replace("/unthread send", "")
+ .trim();
+ }
+
+ const messageDate = message.timestamp || message.createdAt;
+
+ return (
+
+
+
+
+ {displayName}
+
+
+ {!isCustomer ? "Support" : "Customer"}
+
+
+
+ {format(
+ new Date(messageDate),
+ "MMM d, yyyy 'at' h:mm a",
+ )}
+
+
+
+
+ );
+ })
+ ) : (
+
+
+ No messages yet for this ticket.
+
+
+ )}
+
+
+
+ {/* Reply Section */}
+ {ticket.status !== "closed" && ticket.status !== "resolved" ? (
+
+
+ Reply to this case
+
+
+
+
+
+
+ Markdown Supported
+
+
+
+
+ ) : (
+
+
+ This ticket is closed. If you need further assistance,
+ please create a new ticket.
+
+
+ )}
+
+
+
+
+
+ );
+}
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/SupportCasesClient.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/SupportCasesClient.tsx
new file mode 100644
index 00000000000..7e6b455fda0
--- /dev/null
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/SupportCasesClient.tsx
@@ -0,0 +1,169 @@
+"use client";
+
+import { PlusIcon } from "lucide-react";
+import { useState } from "react";
+import type { SupportTicket } from "@/api/support";
+import type { Team } from "@/api/team";
+import { Badge } from "@/components/ui/badge";
+import { useDashboardRouter } from "@/lib/DashboardRouter";
+import { getStatusColor, getStatusLabel } from "@/lib/support-utils";
+import { SupportHeader } from "./SupportHeader";
+import { SupportTabs } from "./SupportTabs";
+
+interface SupportCasesClientProps {
+ tickets: SupportTicket[];
+ team: Team;
+}
+
+export default function SupportCasesClient({
+ tickets,
+ team,
+}: SupportCasesClientProps) {
+ const [activeTab, setActiveTab] = useState("all");
+ const [searchQuery, setSearchQuery] = useState("");
+ const router = useDashboardRouter();
+
+ const handleCreateCase = () => {
+ router.push(`/team/${team.slug}/~/support/create`);
+ };
+
+ // Filter tickets based on active tab and search query
+ const filteredTickets = tickets.filter((ticket) => {
+ // Filter by tab
+ let matchesTab = true;
+ switch (activeTab) {
+ case "open":
+ matchesTab =
+ (ticket.status as string) === "in_progress" ||
+ (ticket.status as string) === "needs_response" ||
+ (ticket.status as string) === "on_hold";
+ break;
+ case "closed":
+ matchesTab = ticket.status === "resolved" || ticket.status === "closed";
+ break;
+ default:
+ matchesTab = true;
+ }
+
+ // Filter by search query
+ const matchesSearch =
+ searchQuery === "" ||
+ ticket.id.toLowerCase().includes(searchQuery.toLowerCase());
+
+ return matchesTab && matchesSearch;
+ });
+
+ // Calculate counts for tabs using inline search logic
+ const counts = {
+ all: tickets.filter(
+ (ticket) =>
+ searchQuery === "" ||
+ ticket.id.toLowerCase().includes(searchQuery.toLowerCase()),
+ ).length,
+ closed: tickets.filter(
+ (ticket) =>
+ (ticket.status === "resolved" || ticket.status === "closed") &&
+ (searchQuery === "" ||
+ ticket.id.toLowerCase().includes(searchQuery.toLowerCase())),
+ ).length,
+ open: tickets.filter(
+ (ticket) =>
+ ((ticket.status as string) === "in_progress" ||
+ (ticket.status as string) === "needs_response" ||
+ (ticket.status as string) === "on_hold") &&
+ (searchQuery === "" ||
+ ticket.id.toLowerCase().includes(searchQuery.toLowerCase())),
+ ).length,
+ };
+
+ const handleSelectCase = (ticketId: string) => {
+ router.push(`/team/${team.slug}/~/support/cases/${ticketId}`);
+ };
+
+ return (
+
+
+
+
+
+
+ {filteredTickets.length === 0 ? (
+
+
+
+ No cases found
+
+
+ {activeTab === "all"
+ ? "You don't have any support cases yet."
+ : `No ${activeTab} cases found.`}
+
+
+
+ ) : (
+
+ {filteredTickets.map((ticket) => (
+
handleSelectCase(ticket.id)}
+ type="button"
+ >
+
+
+
+ {getStatusLabel(ticket.status)}
+
+
+
+
+
+ Ticket #{ticket.id}
+
+
+
+
+ {/* Assuming ticket.updatedAt or ticket.createdAt exists */}
+ {/* If not, you might need to adjust this line */}
+ {/* For now, using a placeholder or assuming one of them is available */}
+ {/* If both are null, it will show "N/A" */}
+ {ticket.updatedAt
+ ? new Date(ticket.updatedAt).toLocaleDateString()
+ : ticket.createdAt
+ ? new Date(ticket.createdAt).toLocaleDateString()
+ : "N/A"}
+
+
+
+ ))}
+
+ )}
+
+
+ );
+}
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/SupportHeader.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/SupportHeader.tsx
new file mode 100644
index 00000000000..e5b1cf8a3a9
--- /dev/null
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/SupportHeader.tsx
@@ -0,0 +1,47 @@
+import type { LucideIcon } from "lucide-react";
+import { Button } from "@/components/ui/button";
+import { UnderlineLink } from "@/components/ui/UnderlineLink";
+
+interface SupportHeaderProps {
+ buttonText?: string;
+ buttonIcon?: LucideIcon;
+ onButtonClick?: () => void;
+}
+
+export function SupportHeader({
+ buttonText,
+ buttonIcon: ButtonIcon,
+ onButtonClick,
+}: SupportHeaderProps) {
+ return (
+
+
+
+
Support
+
+
+ Create and view support cases for your projects
+
+
+ Read our Documentation
+
+
+
+ {buttonText && ButtonIcon && onButtonClick && (
+
+
+ {buttonText}
+
+ )}
+
+
+ );
+}
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/SupportTabs.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/SupportTabs.tsx
new file mode 100644
index 00000000000..1aa52e250b2
--- /dev/null
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/SupportTabs.tsx
@@ -0,0 +1,67 @@
+import { SearchIcon } from "lucide-react";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { cn } from "@/lib/utils";
+
+interface SupportTabsProps {
+ activeTab: string;
+ onTabChange: (tab: string) => void;
+ searchQuery: string;
+ onSearchChange: (query: string) => void;
+ counts: {
+ all: number;
+ open: number;
+ closed: number;
+ };
+}
+
+export function SupportTabs({
+ activeTab,
+ onTabChange,
+ searchQuery,
+ onSearchChange,
+ counts,
+}: SupportTabsProps) {
+ const tabs = [
+ { count: counts.all, id: "all", label: "All" },
+ { count: counts.open, id: "open", label: "Open" },
+ { count: counts.closed, id: "closed", label: "Closed" },
+ ];
+
+ return (
+
+ {/* Search Bar */}
+
+
+ onSearchChange(e.target.value)}
+ placeholder="Search cases..."
+ value={searchQuery}
+ />
+
+
+ {/* Tab Buttons */}
+
+ {tabs.map((tab) => (
+ onTabChange(tab.id)}
+ variant="ghost"
+ >
+
+ {tab.label} ({tab.count})
+
+ {tab.label}
+
+ ))}
+
+
+ );
+}
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/SupportTicketForm.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/SupportTicketForm.tsx
new file mode 100644
index 00000000000..59da99040f2
--- /dev/null
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/SupportTicketForm.tsx
@@ -0,0 +1,209 @@
+import { LoaderCircleIcon } from "lucide-react";
+import dynamic from "next/dynamic";
+import { useRef, useState } from "react";
+import { toast } from "sonner";
+import { revalidatePathAction } from "@/actions/revalidate";
+import { createSupportTicket } from "@/api/support";
+import type { Team } from "@/api/team";
+import { Button } from "@/components/ui/button";
+import { Skeleton } from "@/components/ui/skeleton";
+import { SupportForm_SelectInput } from "./shared/SupportForm_SelectInput";
+
+// Dynamic imports for contact forms using named exports
+const ConnectSupportForm = dynamic(
+ () => import("./contact-forms/connect").then((mod) => mod.ConnectSupportForm),
+ {
+ loading: () => ,
+ ssr: false,
+ },
+);
+const EngineSupportForm = dynamic(
+ () => import("./contact-forms/engine").then((mod) => mod.EngineSupportForm),
+ {
+ loading: () => ,
+ ssr: false,
+ },
+);
+const ContractSupportForm = dynamic(
+ () =>
+ import("./contact-forms/contracts").then((mod) => mod.ContractSupportForm),
+ {
+ loading: () => ,
+ ssr: false,
+ },
+);
+const AccountSupportForm = dynamic(
+ () => import("./contact-forms/account").then((mod) => mod.AccountSupportForm),
+ {
+ loading: () => ,
+ ssr: false,
+ },
+);
+const OtherSupportForm = dynamic(
+ () => import("./contact-forms/other").then((mod) => mod.OtherSupportForm),
+ {
+ loading: () => ,
+ ssr: false,
+ },
+);
+const PaymentsSupportForm = dynamic(
+ () =>
+ import("./contact-forms/payments").then((mod) => mod.PaymentsSupportForm),
+ {
+ loading: () => ,
+ ssr: false,
+ },
+);
+const TokensMarketplaceSupportForm = dynamic(
+ () =>
+ import("./contact-forms/tokens-marketplace").then(
+ (mod) => mod.TokensMarketplaceSupportForm,
+ ),
+ {
+ loading: () => ,
+ ssr: false,
+ },
+);
+
+const productOptions = [
+ { component: , label: "Wallets" },
+ { component: , label: "Transactions" },
+ { component: , label: "Payments" },
+ { component: , label: "Contracts" },
+ {
+ component: ,
+ label: "Tokens / Marketplace",
+ },
+ { component: , label: "Account" },
+ { component: , label: "Other" },
+];
+
+function ProductAreaSelection(props: {
+ productLabel: string;
+ setProductLabel: (val: string) => void;
+}) {
+ const { productLabel, setProductLabel } = props;
+ return (
+
+ o.label)}
+ promptText="Brief description of your issue"
+ required={true}
+ value={productLabel}
+ />
+ {productOptions.find((o) => o.label === productLabel)?.component}
+
+ );
+}
+
+interface SupportTicketFormProps {
+ team: Team;
+ productLabel: string;
+ setProductLabel: (val: string) => void;
+ onSuccess?: () => void;
+ conversationId?: string;
+}
+
+export function SupportTicketForm({
+ team,
+ productLabel,
+ setProductLabel,
+ onSuccess,
+ conversationId,
+}: SupportTicketFormProps) {
+ const [isSubmittingForm, setIsSubmittingForm] = useState(false);
+ const formRef = useRef(null);
+
+ const handleFormSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+
+ if (!productLabel) {
+ toast.error("Please select what you need help with");
+ return;
+ }
+
+ if (!formRef.current) return;
+ const formData = new FormData(formRef.current);
+ const description = formData.get("markdown") as string;
+
+ if (!description?.trim()) {
+ toast.error("Please provide a description");
+ return;
+ }
+
+ setIsSubmittingForm(true);
+
+ try {
+ // Get all extra fields from the form
+ const extraFields = Array.from(formData.entries()).filter(([key]) =>
+ key.startsWith("extraInfo_"),
+ );
+
+ // Format the message
+ let formattedMessage = `Email: ${String(team.billingEmail ?? "-")}`;
+ formattedMessage += `\nName: ${String(team.name ?? "-")}`;
+ formattedMessage += `\nProduct: ${String(productLabel ?? "-")}`;
+
+ // Add all extra fields above the message
+ if (extraFields.length > 0) {
+ extraFields.forEach(([key, value]) => {
+ if (value) {
+ const fieldName = key.replace("extraInfo_", "").replace(/_/g, " ");
+ formattedMessage += `\n${fieldName}: ${String(value)}`;
+ }
+ });
+ }
+
+ formattedMessage += `\nMessage:\n${String(description ?? "-")}`;
+
+ if (conversationId) {
+ formattedMessage += `\n\n---\nAI Conversation ID: ${conversationId}`;
+ }
+
+ await createSupportTicket({
+ message: formattedMessage,
+ teamSlug: team.slug,
+ title: `${productLabel} Issue - ${team.billingEmail} (${team.billingPlan})`,
+ conversationId: conversationId,
+ });
+
+ // Revalidate the support page to show the newly created case
+ await revalidatePathAction(`/team/${team.slug}/support`, "page");
+
+ if (onSuccess) onSuccess();
+ if (formRef.current) formRef.current.reset();
+ setProductLabel("");
+ toast.success("Support ticket created successfully!");
+ } catch (error) {
+ console.error("Error creating support ticket:", error);
+ toast.error("Failed to create support ticket. Please try again.");
+ } finally {
+ setIsSubmittingForm(false);
+ }
+ };
+
+ return (
+
+ );
+}
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/contact-forms/account/index.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/contact-forms/account/index.tsx
new file mode 100644
index 00000000000..f88f8dc479f
--- /dev/null
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/contact-forms/account/index.tsx
@@ -0,0 +1,34 @@
+"use client";
+
+import { useState } from "react";
+import { DescriptionInput } from "../../shared/SupportForm_DescriptionInput";
+import { SupportForm_SelectInput } from "../../shared/SupportForm_SelectInput";
+
+const ACCOUNT_PROBLEM_AREAS = [
+ "Pricing inquiry",
+ "Billing inquiry",
+ "Usage inquiry",
+ "Other",
+];
+
+export function AccountSupportForm() {
+ const [problemArea, setProblemArea] = useState("");
+ const [description, setDescription] = useState("");
+
+ return (
+ <>
+
+ {problemArea && (
+
+ )}
+ >
+ );
+}
diff --git a/apps/dashboard/src/app/(app)/(dashboard)/support/create-ticket/components/contact-forms/connect/AffectedAreaInput.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/contact-forms/connect/AffectedAreaInput.tsx
similarity index 71%
rename from apps/dashboard/src/app/(app)/(dashboard)/support/create-ticket/components/contact-forms/connect/AffectedAreaInput.tsx
rename to apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/contact-forms/connect/AffectedAreaInput.tsx
index a7b2485451e..9f0d8170098 100644
--- a/apps/dashboard/src/app/(app)/(dashboard)/support/create-ticket/components/contact-forms/connect/AffectedAreaInput.tsx
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/contact-forms/connect/AffectedAreaInput.tsx
@@ -1,15 +1,19 @@
+"use client";
+
import { useState } from "react";
-import { AttachmentForm } from "../shared/SupportForm_AttachmentUploader";
-import { DescriptionInput } from "../shared/SupportForm_DescriptionInput";
-import { SupportForm_SelectInput } from "../shared/SupportForm_SelectInput";
-import { SupportForm_TextInput } from "../shared/SupportForm_TextInput";
-import { UnitySupportForm } from "../shared/SupportForm_UnityInput";
+import { DescriptionInput } from "../../shared/SupportForm_DescriptionInput";
+import { SupportForm_SelectInput } from "../../shared/SupportForm_SelectInput";
+import { SupportForm_TextInput } from "../../shared/SupportForm_TextInput";
+import { UnitySupportForm } from "../../shared/SupportForm_UnityInput";
const AFFECTED_AREAS = ["Dashboard", "Application"];
export const AffectedAreaInput = () => {
const [selectedAffectedArea, setSelectedAffectedArea] = useState("");
const [selectedSDK, setSelectedSDK] = useState("");
+ const [description, setDescription] = useState("");
+ const [sdkDescription, setSdkDescription] = useState("");
+
return (
<>
{
inputType="url"
required={false}
/>
-
-
+
>
)}
>
) : (
- <>
-
-
- >
+
))}
>
);
diff --git a/apps/dashboard/src/app/(app)/(dashboard)/support/create-ticket/components/contact-forms/connect/index.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/contact-forms/connect/index.tsx
similarity index 76%
rename from apps/dashboard/src/app/(app)/(dashboard)/support/create-ticket/components/contact-forms/connect/index.tsx
rename to apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/contact-forms/connect/index.tsx
index a47f4f6a6ad..5e61bb26d14 100644
--- a/apps/dashboard/src/app/(app)/(dashboard)/support/create-ticket/components/contact-forms/connect/index.tsx
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/contact-forms/connect/index.tsx
@@ -1,9 +1,8 @@
import { type ReactElement, useState } from "react";
-import { AttachmentForm } from "../shared/SupportForm_AttachmentUploader";
-import { DescriptionInput } from "../shared/SupportForm_DescriptionInput";
-import { SupportForm_SelectInput } from "../shared/SupportForm_SelectInput";
-import { SupportForm_TextInput } from "../shared/SupportForm_TextInput";
-import { UnitySupportForm } from "../shared/SupportForm_UnityInput";
+import { DescriptionInput } from "../../shared/SupportForm_DescriptionInput";
+import { SupportForm_SelectInput } from "../../shared/SupportForm_SelectInput";
+import { SupportForm_TextInput } from "../../shared/SupportForm_TextInput";
+import { UnitySupportForm } from "../../shared/SupportForm_UnityInput";
import { AffectedAreaInput } from "./AffectedAreaInput";
type ProblemAreaItem = {
@@ -35,27 +34,28 @@ const OSSelect = () => {
);
};
+const DescriptionInputWrapper = () => {
+ const [description, setDescription] = useState("");
+ return ;
+};
+
const PROBLEM_AREAS: ProblemAreaItem[] = [
{
component: ,
- label: "Embedded wallet login issues",
+ label: "In-app wallet login issues",
},
{
component: ,
- label: "Embedded wallet transaction issues",
+ label: "In-app wallet transaction issues",
},
{
component: ,
- label: "Embedded wallet Custom Auth",
+ label: "In-app wallet Custom Auth",
},
{
component: ,
label: "Account Abstraction",
},
- {
- component: ,
- label: "In-app wallet",
- },
{
component: (
<>
@@ -66,8 +66,7 @@ const PROBLEM_AREAS: ProblemAreaItem[] = [
inputType="url"
required={false}
/>
-
-
+
>
),
label: "Connect SDKs",
@@ -77,8 +76,7 @@ const PROBLEM_AREAS: ProblemAreaItem[] = [
<>
-
-
+
>
),
label: "Unity SDK",
@@ -101,8 +99,7 @@ const PROBLEM_AREAS: ProblemAreaItem[] = [
inputType="text"
required={false}
/>
-
-
+
>
),
label: ".NET SDK",
@@ -117,7 +114,7 @@ const PROBLEM_AREAS: ProblemAreaItem[] = [
},
];
-export default function ConnectSupportForm() {
+export function ConnectSupportForm() {
const [selectedProblemArea, setSelectedProblemArea] = useState("");
return (
diff --git a/apps/dashboard/src/app/(app)/(dashboard)/support/create-ticket/components/contact-forms/contracts/index.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/contact-forms/contracts/index.tsx
similarity index 80%
rename from apps/dashboard/src/app/(app)/(dashboard)/support/create-ticket/components/contact-forms/contracts/index.tsx
rename to apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/contact-forms/contracts/index.tsx
index c06eeea8077..d9413073ed0 100644
--- a/apps/dashboard/src/app/(app)/(dashboard)/support/create-ticket/components/contact-forms/contracts/index.tsx
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/contact-forms/contracts/index.tsx
@@ -1,8 +1,7 @@
import { type ReactElement, useState } from "react";
-import { AttachmentForm } from "../shared/SupportForm_AttachmentUploader";
-import { DescriptionInput } from "../shared/SupportForm_DescriptionInput";
-import { SupportForm_SelectInput } from "../shared/SupportForm_SelectInput";
-import { SupportForm_TextInput } from "../shared/SupportForm_TextInput";
+import { DescriptionInput } from "../../shared/SupportForm_DescriptionInput";
+import { SupportForm_SelectInput } from "../../shared/SupportForm_SelectInput";
+import { SupportForm_TextInput } from "../../shared/SupportForm_TextInput";
type ProblemAreaItem = {
label: string;
@@ -62,6 +61,11 @@ const ContractAffectedAreaInput = () => {
);
};
+const DescriptionInputWrapper = () => {
+ const [description, setDescription] = useState("");
+ return ;
+};
+
const CONTRACT_PROBLEM_AREAS: ProblemAreaItem[] = [
{
component: (
@@ -69,8 +73,7 @@ const CONTRACT_PROBLEM_AREAS: ProblemAreaItem[] = [
-
-
+
>
),
label: "Deploying a contract",
@@ -81,8 +84,7 @@ const CONTRACT_PROBLEM_AREAS: ProblemAreaItem[] = [
-
-
+
>
),
label: "Contract verification",
@@ -94,8 +96,7 @@ const CONTRACT_PROBLEM_AREAS: ProblemAreaItem[] = [
-
-
+
>
),
label: "Calling a function in my contract",
@@ -103,8 +104,7 @@ const CONTRACT_PROBLEM_AREAS: ProblemAreaItem[] = [
{
component: (
<>
-
-
+
>
),
label: "Developing a custom contract",
@@ -112,15 +112,14 @@ const CONTRACT_PROBLEM_AREAS: ProblemAreaItem[] = [
{
component: (
<>
-
-
+
>
),
label: "Other",
},
];
-export default function ContractSupportForm() {
+export function ContractSupportForm() {
const [problemArea, setProblemArea] = useState("");
return (
<>
diff --git a/apps/dashboard/src/app/(app)/(dashboard)/support/create-ticket/components/contact-forms/engine/index.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/contact-forms/engine/index.tsx
similarity index 72%
rename from apps/dashboard/src/app/(app)/(dashboard)/support/create-ticket/components/contact-forms/engine/index.tsx
rename to apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/contact-forms/engine/index.tsx
index 860765bccfc..4ba340c4614 100644
--- a/apps/dashboard/src/app/(app)/(dashboard)/support/create-ticket/components/contact-forms/engine/index.tsx
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/contact-forms/engine/index.tsx
@@ -1,10 +1,9 @@
import { useState } from "react";
-import { AttachmentForm } from "../shared/SupportForm_AttachmentUploader";
-import { DescriptionInput } from "../shared/SupportForm_DescriptionInput";
-import { SupportForm_SelectInput } from "../shared/SupportForm_SelectInput";
-import { SupportForm_TextInput } from "../shared/SupportForm_TextInput";
+import { DescriptionInput } from "../../shared/SupportForm_DescriptionInput";
+import { SupportForm_SelectInput } from "../../shared/SupportForm_SelectInput";
+import { SupportForm_TextInput } from "../../shared/SupportForm_TextInput";
-const ENGINE_TYPES = ["Cloud-Hosted", "Self-Hosted"];
+const ENGINE_TYPES = ["Cloud (V3)", "Dedicated (V2)"];
const ENGINE_PROBLEM_AREAS = [
"SSL Issues",
"Transaction queueing issues",
@@ -13,20 +12,22 @@ const ENGINE_PROBLEM_AREAS = [
"Other",
];
-export default function EngineSupportForm() {
+export function EngineSupportForm() {
const [selectedEngineType, setSelectedEngineType] = useState("");
const [problemArea, setProblemArea] = useState("");
+ const [description, setDescription] = useState("");
+
return (
<>
+ />{" "}
{selectedEngineType && (
<>
-
-
+
>
)}
>
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/contact-forms/other/index.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/contact-forms/other/index.tsx
new file mode 100644
index 00000000000..075b94f55a1
--- /dev/null
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/contact-forms/other/index.tsx
@@ -0,0 +1,36 @@
+import { useState } from "react";
+import { DescriptionInput } from "../../shared/SupportForm_DescriptionInput";
+import { SupportForm_SelectInput } from "../../shared/SupportForm_SelectInput";
+
+const SharedOtherProblemComponent = () => {
+ const [description, setDescription] = useState("");
+
+ return ;
+};
+
+const OTHER_PROBLEM_AREAS = [
+ "General inquiry",
+ "Feature request",
+ "Bug report",
+ "Documentation",
+ "Integration help",
+ "Other",
+];
+export function OtherSupportForm() {
+ const [problemArea, setProblemArea] = useState("");
+
+ return (
+ <>
+
+ {problemArea && }
+ >
+ );
+}
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/contact-forms/payments/index.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/contact-forms/payments/index.tsx
new file mode 100644
index 00000000000..052245261b1
--- /dev/null
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/contact-forms/payments/index.tsx
@@ -0,0 +1,55 @@
+"use client";
+
+import { useState } from "react";
+import { DescriptionInput } from "../../shared/SupportForm_DescriptionInput";
+import { SupportForm_SelectInput } from "../../shared/SupportForm_SelectInput";
+import { SupportForm_TextInput } from "../../shared/SupportForm_TextInput";
+
+const PAYMENT_AREAS = ["Dashboard", "Application"];
+
+export function PaymentsSupportForm() {
+ const [area, setArea] = useState("");
+ const [description, setDescription] = useState("");
+
+ return (
+ <>
+
+ {area === "Application" && (
+ <>
+
+
+
+ >
+ )}
+ {(area === "Application" || area === "Dashboard") && (
+
+ )}
+ >
+ );
+}
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/contact-forms/tokens-marketplace/index.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/contact-forms/tokens-marketplace/index.tsx
new file mode 100644
index 00000000000..89d80267649
--- /dev/null
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/contact-forms/tokens-marketplace/index.tsx
@@ -0,0 +1,27 @@
+import { useState } from "react";
+import { DescriptionInput } from "../../shared/SupportForm_DescriptionInput";
+import { SupportForm_TextInput } from "../../shared/SupportForm_TextInput";
+
+export function TokensMarketplaceSupportForm() {
+ const [description, setDescription] = useState("");
+
+ return (
+ <>
+
+
+
+ >
+ );
+}
diff --git a/apps/dashboard/src/app/(app)/(dashboard)/support/create-ticket/components/contact-forms/shared/SupportForm_DescriptionInput.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/shared/SupportForm_DescriptionInput.tsx
similarity index 50%
rename from apps/dashboard/src/app/(app)/(dashboard)/support/create-ticket/components/contact-forms/shared/SupportForm_DescriptionInput.tsx
rename to apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/shared/SupportForm_DescriptionInput.tsx
index 24bed8dd13f..5f9e9c747de 100644
--- a/apps/dashboard/src/app/(app)/(dashboard)/support/create-ticket/components/contact-forms/shared/SupportForm_DescriptionInput.tsx
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/shared/SupportForm_DescriptionInput.tsx
@@ -1,23 +1,31 @@
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
-
-type Props = {
- placeholder?: string;
-};
+import { cn } from "@/lib/utils";
const defaultDescription =
"Please describe the issue you're encountering in detail, including steps that led to the error, any error messages, troubleshooting steps you've already taken, and the product(s), dashboard, or SDKs involved.";
-export const DescriptionInput = (props: Props) => {
+interface Props {
+ value: string;
+ onChange: (value: string) => void;
+ className?: string;
+ placeholder?: string;
+}
+
+export function DescriptionInput(props: Props) {
return (
-
-
+
+
Description
- •
+ •
);
-};
+}
diff --git a/apps/dashboard/src/app/(app)/(dashboard)/support/create-ticket/components/contact-forms/shared/SupportForm_SelectInput.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/shared/SupportForm_SelectInput.tsx
similarity index 60%
rename from apps/dashboard/src/app/(app)/(dashboard)/support/create-ticket/components/contact-forms/shared/SupportForm_SelectInput.tsx
rename to apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/shared/SupportForm_SelectInput.tsx
index fdea3a49fbb..5a00fa72085 100644
--- a/apps/dashboard/src/app/(app)/(dashboard)/support/create-ticket/components/contact-forms/shared/SupportForm_SelectInput.tsx
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/shared/SupportForm_SelectInput.tsx
@@ -21,11 +21,14 @@ export const SupportForm_SelectInput = (props: Props) => {
const { options, formLabel, name, required, promptText } = props;
return (
-
-
+
+
{formLabel}
{required && (
- •
+ •
)}
@@ -37,12 +40,16 @@ export const SupportForm_SelectInput = (props: Props) => {
required={required}
value={props.value}
>
-
+
{props.value}
-
+
{options.map((option) => (
-
+
{option}
))}
diff --git a/apps/dashboard/src/app/(app)/(dashboard)/support/create-ticket/components/contact-forms/shared/SupportForm_TextInput.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/shared/SupportForm_TextInput.tsx
similarity index 56%
rename from apps/dashboard/src/app/(app)/(dashboard)/support/create-ticket/components/contact-forms/shared/SupportForm_TextInput.tsx
rename to apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/shared/SupportForm_TextInput.tsx
index 4fea62f0a99..5656534e6d9 100644
--- a/apps/dashboard/src/app/(app)/(dashboard)/support/create-ticket/components/contact-forms/shared/SupportForm_TextInput.tsx
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/shared/SupportForm_TextInput.tsx
@@ -8,19 +8,26 @@ type Props = {
required: boolean;
inputType: HTMLInputTypeAttribute | undefined;
placeholder?: string;
+ className?: string;
};
+
export const SupportForm_TextInput = (props: Props) => {
- const { formLabel, formValue, required, placeholder, inputType } = props;
+ const { formLabel, formValue, required, placeholder, inputType, className } =
+ props;
return (
-
-
+
+
{formLabel}
{required && (
- •
+ •
)}
;
+}) {
+ const params = await props.params;
+
+ const [team, token] = await Promise.all([
+ getTeamBySlug(params.team_slug),
+ getAuthToken(),
+ ]);
+
+ if (!team || !token) {
+ notFound();
+ }
+
+ // Fetch ticket details
+ let ticket: SupportTicket | null = null;
+
+ try {
+ ticket = await getSupportTicket(params.id, params.team_slug, token);
+ } catch (error) {
+ console.error("Failed to load ticket:", error);
+ notFound();
+ }
+
+ if (!ticket) {
+ notFound();
+ }
+
+ return ;
+}
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/create/page.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/create/page.tsx
new file mode 100644
index 00000000000..3aebc4617d3
--- /dev/null
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/create/page.tsx
@@ -0,0 +1,23 @@
+import { notFound } from "next/navigation";
+import { getAuthToken } from "@/api/auth-token";
+import { getTeamBySlug } from "@/api/team";
+import { CreateSupportCase } from "../_components/CreateSupportCase";
+
+export default async function CreatePage(props: {
+ params: Promise<{
+ team_slug: string;
+ }>;
+}) {
+ const params = await props.params;
+
+ const [team, token] = await Promise.all([
+ getTeamBySlug(params.team_slug),
+ getAuthToken(),
+ ]);
+
+ if (!team || !token) {
+ notFound();
+ }
+
+ return ;
+}
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/layout.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/layout.tsx
new file mode 100644
index 00000000000..c5784b288a2
--- /dev/null
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/layout.tsx
@@ -0,0 +1,37 @@
+import { redirect } from "next/navigation";
+import { getAuthToken } from "@/api/auth-token";
+import { getTeamBySlug } from "@/api/team";
+import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
+import { loginRedirect } from "@/utils/redirects";
+import { getValidAccount } from "../../../../../account/settings/getAccount";
+import { SupportLayout } from "./SupportLayout";
+
+export default async function Layout({
+ params,
+ children,
+}: {
+ params: Promise<{ team_slug: string }>;
+ children: React.ReactNode;
+}) {
+ const resolvedParams = await params;
+ const [_account, team, authToken] = await Promise.all([
+ getValidAccount(`/team/${resolvedParams.team_slug}/~/support`),
+ getTeamBySlug(resolvedParams.team_slug),
+ getAuthToken(),
+ ]);
+
+ if (!authToken) {
+ loginRedirect(`/team/${resolvedParams.team_slug}/~/support`);
+ }
+
+ if (!team) {
+ redirect("/team");
+ }
+
+ const _client = getClientThirdwebClient({
+ jwt: authToken,
+ teamId: team.id,
+ });
+
+ return {children} ;
+}
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/page.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/page.tsx
new file mode 100644
index 00000000000..940de714c81
--- /dev/null
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/page.tsx
@@ -0,0 +1,34 @@
+import { notFound } from "next/navigation";
+import { getAuthToken } from "@/api/auth-token";
+import { getSupportTicketsByTeam, type SupportTicket } from "@/api/support";
+import { getTeamBySlug } from "@/api/team";
+import SupportCasesClient from "./_components/SupportCasesClient";
+
+export default async function Page(props: {
+ params: Promise<{
+ team_slug: string;
+ }>;
+}) {
+ const params = await props.params;
+
+ const [team, token] = await Promise.all([
+ getTeamBySlug(params.team_slug),
+ getAuthToken(),
+ ]);
+
+ if (!team || !token) {
+ notFound();
+ }
+
+ // Fetch real support tickets for this team using team slug
+ let supportTickets: SupportTicket[] = [];
+
+ try {
+ supportTickets = await getSupportTicketsByTeam(params.team_slug, token);
+ } catch (_error) {
+ // Return empty array instead of crashing the page
+ supportTickets = [];
+ }
+
+ return ;
+}
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/layout.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/layout.tsx
index 0e13ff01443..e356c629e94 100644
--- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/layout.tsx
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/layout.tsx
@@ -107,7 +107,7 @@ export default async function ProjectLayout(props: {
label="Ask AI Assistant"
networks="all"
pageType="support"
- teamId={team.id}
+ team={team}
/>
diff --git a/apps/portal/src/components/Document/AuthMethodsTabs.tsx b/apps/portal/src/components/Document/AuthMethodsTabs.tsx
index 9ccf29d2374..d4fab5fa99b 100644
--- a/apps/portal/src/components/Document/AuthMethodsTabs.tsx
+++ b/apps/portal/src/components/Document/AuthMethodsTabs.tsx
@@ -762,7 +762,7 @@ function AuthMethodsTabsContent() {
>