This commit is contained in:
Abhishek Kumar 2026-02-18 13:16:49 +05:30
parent fe4ea648e4
commit 9ce5a8e5e2
39 changed files with 338 additions and 758 deletions

View file

@ -14,11 +14,7 @@ import {
} from '@/components/ui/dialog';
import { downloadFile, getSignedUrl } from '@/lib/files';
interface MediaPreviewDialogProps {
accessToken: string | null;
}
export function MediaPreviewDialog({ accessToken }: MediaPreviewDialogProps) {
export function MediaPreviewDialog() {
const [isOpen, setIsOpen] = useState(false);
const [audioSignedUrl, setAudioSignedUrl] = useState<string | null>(null);
const [transcriptContent, setTranscriptContent] = useState<string | null>(null);
@ -29,7 +25,7 @@ export function MediaPreviewDialog({ accessToken }: MediaPreviewDialogProps) {
const openPreview = useCallback(
async (recordingUrl: string | null, transcriptUrl: string | null, runId: number) => {
if (!accessToken || (!recordingUrl && !transcriptUrl)) return;
if (!recordingUrl && !transcriptUrl) return;
setMediaLoading(true);
setAudioSignedUrl(null);
setTranscriptContent(null);
@ -39,8 +35,8 @@ export function MediaPreviewDialog({ accessToken }: MediaPreviewDialogProps) {
setIsOpen(true);
const [audioResult, transcriptResult] = await Promise.all([
recordingUrl ? getSignedUrl(recordingUrl, accessToken) : null,
transcriptUrl ? getSignedUrl(transcriptUrl, accessToken, true) : null,
recordingUrl ? getSignedUrl(recordingUrl) : null,
transcriptUrl ? getSignedUrl(transcriptUrl, true) : null,
]);
if (audioResult) {
@ -59,7 +55,7 @@ export function MediaPreviewDialog({ accessToken }: MediaPreviewDialogProps) {
setMediaLoading(false);
},
[accessToken],
[],
);
return {
@ -102,13 +98,13 @@ export function MediaPreviewDialog({ accessToken }: MediaPreviewDialogProps) {
<Button variant="secondary">Close</Button>
</DialogClose>
<div className="flex gap-2">
{recordingKey && accessToken && (
<Button variant="outline" onClick={() => downloadFile(recordingKey, accessToken)}>
{recordingKey && (
<Button variant="outline" onClick={() => downloadFile(recordingKey)}>
Download Recording
</Button>
)}
{transcriptKey && accessToken && (
<Button variant="outline" onClick={() => downloadFile(transcriptKey, accessToken)}>
{transcriptKey && (
<Button variant="outline" onClick={() => downloadFile(transcriptKey)}>
Download Transcript
</Button>
)}

View file

@ -10,7 +10,6 @@ import { Checkbox } from "@/components/ui/checkbox";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { useUserConfig } from "@/context/UserConfigContext";
import { cn } from "@/lib/utils";
// Providers that have MPS voice endpoints
@ -30,7 +29,6 @@ export const VoiceSelector: React.FC<VoiceSelectorProps> = ({
onChange,
className,
}) => {
const { accessToken } = useUserConfig();
const [isOpen, setIsOpen] = useState(false);
const [searchTerm, setSearchTerm] = useState("");
const [isManualInput, setIsManualInput] = useState(false);
@ -60,7 +58,7 @@ export const VoiceSelector: React.FC<VoiceSelectorProps> = ({
const fetchVoices = useCallback(async () => {
const providerKey = getProviderKey(provider);
if (!providerKey || !accessToken) {
if (!providerKey) {
setVoices([]);
return;
}
@ -71,9 +69,6 @@ export const VoiceSelector: React.FC<VoiceSelectorProps> = ({
try {
const response = await getVoicesApiV1UserConfigurationsVoicesProviderGet({
path: { provider: providerKey },
headers: {
Authorization: `Bearer ${accessToken}`,
},
});
if (response.data?.voices) {
@ -86,7 +81,7 @@ export const VoiceSelector: React.FC<VoiceSelectorProps> = ({
} finally {
setIsLoading(false);
}
}, [provider, getProviderKey, accessToken]);
}, [provider, getProviderKey]);
useEffect(() => {
if (provider) {

View file

@ -36,7 +36,7 @@ export function CredentialSelector({
description = "Select a credential for authentication, or leave empty for no auth.",
showLabel = true,
}: CredentialSelectorProps) {
const { getAccessToken } = useAuth();
useAuth();
const [credentials, setCredentials] = useState<CredentialResponse[]>([]);
const [loading, setLoading] = useState(false);
@ -45,10 +45,7 @@ export function CredentialSelector({
const fetchCredentials = useCallback(async () => {
setLoading(true);
try {
const accessToken = await getAccessToken();
const response = await listCredentialsApiV1CredentialsGet({
headers: { Authorization: `Bearer ${accessToken}` },
});
const response = await listCredentialsApiV1CredentialsGet({});
if (response.error) {
console.error("Failed to fetch credentials:", response.error);
setCredentials([]);
@ -63,7 +60,7 @@ export function CredentialSelector({
} finally {
setLoading(false);
}
}, [getAccessToken]);
}, []);
useEffect(() => {
fetchCredentials();

View file

@ -28,44 +28,47 @@ const AppLayout: React.FC<AppLayoutProps> = ({
const isWorkflowEditor = /^\/workflow\/\d+/.test(pathname);
const isSuperadmin = pathname.startsWith("/superadmin");
// If no sidebar needed, just return children
if (!shouldShowSidebar) {
return <>{children}</>;
}
// Always render SidebarProvider to keep the component tree shape consistent
// across route changes (avoids React hooks ordering violations during navigation).
return (
<SidebarProvider defaultOpen={!isWorkflowEditor && !isSuperadmin}>
<div className="flex min-h-screen w-full">
<AppSidebar />
<SidebarInset className="flex-1">
{/* Optional header area for specific pages */}
{headerActions && (
<header className="sticky top-0 z-50 w-full border-b bg-background">
<div className="container mx-auto px-4 py-4">
<div className="flex items-center justify-center">
{headerActions}
{shouldShowSidebar ? (
<div className="flex min-h-screen w-full">
<AppSidebar />
<SidebarInset className="flex-1">
{/* Optional header area for specific pages */}
{headerActions && (
<header className="sticky top-0 z-50 w-full border-b bg-background">
<div className="container mx-auto px-4 py-4">
<div className="flex items-center justify-center">
{headerActions}
</div>
</div>
</header>
)}
{/* Optional sticky tabs */}
{stickyTabs && (
<div className="sticky top-0 z-40 bg-[#2a2e39] border-b border-gray-700">
<div className="container mx-auto px-4">
<div className="flex items-center justify-center py-2">
{stickyTabs}
</div>
</div>
</div>
</header>
)}
)}
{/* Optional sticky tabs */}
{stickyTabs && (
<div className="sticky top-0 z-40 bg-[#2a2e39] border-b border-gray-700">
<div className="container mx-auto px-4">
<div className="flex items-center justify-center py-2">
{stickyTabs}
</div>
</div>
</div>
)}
{/* Main content area */}
<main className="flex-1">
{children}
</main>
</SidebarInset>
</div>
{/* Main content area */}
<main className="flex-1">
{children}
</main>
</SidebarInset>
</div>
) : (
<div className="flex-1 w-full">
{children}
</div>
)}
</SidebarProvider>
);
};

View file

@ -11,9 +11,11 @@ import {
HelpCircle,
Home,
Key,
LogOut,
Megaphone,
MessageSquare,
Phone,
Settings,
Star,
TrendingUp,
Workflow,
@ -26,6 +28,14 @@ import React from "react";
import ThemeToggle from "@/components/ThemeSwitcher";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
Sidebar,
SidebarContent,
@ -50,11 +60,6 @@ import { useAppConfig } from "@/context/AppConfigContext";
import { useAuth } from "@/lib/auth";
import { cn } from "@/lib/utils";
// Conditionally load Stack components only when using Stack auth
const StackUserButton = React.lazy(() =>
import("@stackframe/stack").then((mod) => ({ default: mod.UserButton }))
);
// Lazy load SelectedTeamSwitcher - we'll pass selectedTeam from our context
const StackTeamSwitcher = React.lazy(() =>
import("@stackframe/stack").then((mod) => ({
@ -66,7 +71,7 @@ export function AppSidebar() {
const pathname = usePathname();
const router = useRouter();
const { state } = useSidebar();
const { provider, getSelectedTeam } = useAuth();
const { provider, getSelectedTeam, logout, user } = useAuth();
const { config } = useAppConfig();
// Get selected team for Stack auth (cast to Team type from Stack)
@ -400,31 +405,53 @@ export function AppSidebar() {
</>
)}
{/* User Button for Stack Auth - at the bottom */}
{/* User Button - at the bottom */}
{provider === "stack" && (
<React.Suspense
fallback={
<div className={cn(
"animate-pulse bg-muted rounded",
state === "collapsed" ? "h-8 w-8" : "h-[34px] w-[34px]"
)} />
}
>
<div className={cn(
"flex",
state === "collapsed" ? "justify-center" : "justify-start"
)}>
<StackUserButton
extraItems={[
{
text: "Usage",
icon: <CircleDollarSign strokeWidth={2} size={16} />,
onClick: () => router.push("/usage"),
},
]}
/>
</div>
</React.Suspense>
<div className={cn(
"flex",
state === "collapsed" ? "justify-center" : "justify-start"
)}>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="rounded-full h-8 w-8 cursor-pointer">
<span className="text-xs font-medium">
{(user?.displayName || (user as { primaryEmail?: string })?.primaryEmail || "")
.split(/[\s@]/)
.filter(Boolean)
.slice(0, 2)
.map((s: string) => s[0]?.toUpperCase())
.join("")
|| "U"}
</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent side="top" align="start" className="w-56">
<DropdownMenuLabel className="font-normal">
<div className="flex flex-col space-y-1">
{user?.displayName && (
<p className="text-sm font-medium">{user.displayName}</p>
)}
{(user as { primaryEmail?: string })?.primaryEmail && (
<p className="text-xs text-muted-foreground">{(user as { primaryEmail?: string }).primaryEmail}</p>
)}
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => router.push("/handler/account-settings")} className="cursor-pointer">
<Settings className="mr-2 h-4 w-4" />
Account settings
</DropdownMenuItem>
<DropdownMenuItem onClick={() => router.push("/usage")} className="cursor-pointer">
<CircleDollarSign className="mr-2 h-4 w-4" />
Usage
</DropdownMenuItem>
<DropdownMenuItem onClick={() => logout()} className="cursor-pointer">
<LogOut className="mr-2 h-4 w-4" />
Sign out
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
)}
{/* Theme Toggle - at the very bottom */}

View file

@ -19,20 +19,16 @@ export function ConversationsList({ testSessionId }: ConversationsListProps) {
const [conversations, setConversations] = useState<Conversation[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const { user, getAccessToken } = useAuth();
const { user } = useAuth();
useEffect(() => {
const fetchConversations = async () => {
if (!user) return;
try {
const accessToken = await getAccessToken();
const response = await getTestSessionConversationApiV1LooptalkTestSessionsTestSessionIdConversationGet({
path: {
test_session_id: testSessionId
},
headers: {
'Authorization': `Bearer ${accessToken}`,
},
});
// API returns { conversation: Conversation | null }
@ -56,7 +52,7 @@ export function ConversationsList({ testSessionId }: ConversationsListProps) {
// Poll for updates every 5 seconds
const interval = setInterval(fetchConversations, 5000);
return () => clearInterval(interval);
}, [testSessionId, user, getAccessToken]);
}, [testSessionId, user]);
if (loading && conversations.length === 0) {
return (

View file

@ -33,7 +33,7 @@ export function CreateTestSessionButton() {
const router = useRouter();
const [open, setOpen] = useState(false);
const [loading, setLoading] = useState(false);
const { user, getAccessToken } = useAuth();
const { user } = useAuth();
const [formData, setFormData] = useState({
name: '',
description: '',
@ -49,7 +49,6 @@ export function CreateTestSessionButton() {
try {
if (!user) return;
const accessToken = await getAccessToken();
const response = await createTestSessionApiV1LooptalkTestSessionsPost({
body: {
name: formData.name,
@ -61,9 +60,6 @@ export function CreateTestSessionButton() {
concurrent_pairs: formData.test_type === 'load_test' ? formData.concurrent_pairs : undefined
}
},
headers: {
'Authorization': `Bearer ${accessToken}`,
},
});
toast.success('Test session created successfully');

View file

@ -18,7 +18,7 @@ interface CampaignRunsProps {
export function CampaignRuns({ campaignId, workflowId, searchParams }: CampaignRunsProps) {
const router = useRouter();
const { getAccessToken } = useAuth();
const { isAuthenticated } = useAuth();
const [runs, setRuns] = useState<WorkflowRunResponseSchema[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
@ -29,7 +29,6 @@ export function CampaignRuns({ campaignId, workflowId, searchParams }: CampaignR
const [totalPages, setTotalPages] = useState(1);
const [totalCount, setTotalCount] = useState(0);
const [isExecutingFilters, setIsExecutingFilters] = useState(false);
const [accessToken, setAccessToken] = useState<string | null>(null);
// Sort state (initialized from URL)
const [sortBy, setSortBy] = useState<string | null>(() => {
@ -50,22 +49,13 @@ export function CampaignRuns({ campaignId, workflowId, searchParams }: CampaignR
return searchParams ? decodeFiltersFromURL(searchParams, availableAttributes) : [];
});
// Get access token on mount
useEffect(() => {
const fetchToken = async () => {
const token = await getAccessToken();
setAccessToken(token);
};
fetchToken();
}, [getAccessToken]);
const fetchCampaignRuns = useCallback(async (
page: number,
filters?: ActiveFilter[],
sortByParam?: string | null,
sortOrderParam?: 'asc' | 'desc'
) => {
if (!accessToken) return;
if (!isAuthenticated) return;
try {
setLoading(true);
// Prepare filter data for API
@ -88,9 +78,6 @@ export function CampaignRuns({ campaignId, workflowId, searchParams }: CampaignR
...(sortByParam && { sort_by: sortByParam }),
...(sortOrderParam && { sort_order: sortOrderParam }),
},
headers: {
'Authorization': `Bearer ${accessToken}`,
}
});
if (response.error) {
@ -111,7 +98,7 @@ export function CampaignRuns({ campaignId, workflowId, searchParams }: CampaignR
} finally {
setLoading(false);
}
}, [campaignId, accessToken]);
}, [campaignId, isAuthenticated]);
const updatePageInUrl = useCallback((page: number, filters?: ActiveFilter[], sortByParam?: string | null, sortOrderParam?: 'asc' | 'desc') => {
const params = new URLSearchParams();
@ -136,10 +123,10 @@ export function CampaignRuns({ campaignId, workflowId, searchParams }: CampaignR
}, [router, campaignId]);
useEffect(() => {
if (accessToken) {
if (isAuthenticated) {
fetchCampaignRuns(currentPage, appliedFilters, sortBy, sortOrder);
}
}, [currentPage, appliedFilters, fetchCampaignRuns, accessToken, sortBy, sortOrder]);
}, [currentPage, appliedFilters, fetchCampaignRuns, isAuthenticated, sortBy, sortOrder]);
const handleApplyFilters = useCallback(async () => {
setIsExecutingFilters(true);
@ -213,7 +200,6 @@ export function CampaignRuns({ campaignId, workflowId, searchParams }: CampaignR
sortOrder={sortOrder}
onSort={handleSort}
workflowId={workflowId}
accessToken={accessToken}
onReload={handleReload}
title="Campaign Workflow Runs"
emptyMessage="No workflow runs found for this campaign"

View file

@ -47,7 +47,6 @@ export interface WorkflowRunsTableProps {
// Navigation & Actions
workflowId: number;
accessToken: string | null;
// Reload
onReload?: () => void;
@ -78,7 +77,6 @@ export function WorkflowRunsTable({
sortOrder = 'desc',
onSort,
workflowId,
accessToken,
onReload,
title = "Workflow Run History",
subtitle,
@ -88,7 +86,7 @@ export function WorkflowRunsTable({
const [selectedRowId, setSelectedRowId] = useState<number | null>(null);
// Media preview dialog
const mediaPreview = MediaPreviewDialog({ accessToken });
const mediaPreview = MediaPreviewDialog();
const formatDate = (dateString: string) => new Date(dateString).toLocaleString();

View file

@ -15,8 +15,6 @@ import {
TableHeader,
TableRow,
} from "@/components/ui/table";
import { useUserConfig } from '@/context/UserConfigContext';
interface Workflow {
id: number;
name: string;
@ -32,7 +30,6 @@ interface WorkflowTableProps {
export function WorkflowTable({ workflows, showArchived }: WorkflowTableProps) {
const router = useRouter();
const { accessToken } = useUserConfig();
const [isPending, startTransition] = useTransition();
const [loadingWorkflowId, setLoadingWorkflowId] = useState<number | null>(null);
@ -41,11 +38,6 @@ export function WorkflowTable({ workflows, showArchived }: WorkflowTableProps) {
};
const handleArchiveToggle = async (id: number, currentStatus: string) => {
if (!accessToken) {
toast.error('Authentication required');
return;
}
const newStatus = currentStatus === 'active' ? 'archived' : 'active';
const action = currentStatus === 'active' ? 'Archive' : 'Restore';
@ -59,9 +51,6 @@ export function WorkflowTable({ workflows, showArchived }: WorkflowTableProps) {
body: {
status: newStatus,
},
headers: {
'Authorization': `Bearer ${accessToken}`,
},
});
if (response.data) {