mirror of
https://github.com/dograh-hq/dograh.git
synced 2026-07-04 10:52:17 +02:00
fix: changes to update pipecat version to 0.0.100 (#122)
* feat: add stt evals * add smart turn as provider * chore: remove deprecations * chore: format files * fix: remove deprecated UserIdleProcessor * fix: remove deprecated TranscriptProcessor * chore: update pipecat submodule * feat: add evals visualisation * fix: trigger llm generation on client connected and pipeline started * chore: update pipecat * chore: update pipecat submodule * Add tests * fix: slow loading of workflow page * chore: update pipecat submodule * Show version after release * Fixes #99 * fix: provider check for websocket connection * Fixes #107 * Fix #96 * chore: fix documentation * fix: cloudonix campaign call error --------- Co-authored-by: Sabiha Khan <sabihak89@gmail.com>
This commit is contained in:
parent
a4367bd83b
commit
911c5ed416
104 changed files with 16919 additions and 597 deletions
|
|
@ -1,6 +1,6 @@
|
|||
import { redirect } from "next/navigation";
|
||||
|
||||
import { getWorkflowsApiV1WorkflowFetchGet } from "@/client/sdk.gen";
|
||||
import { getWorkflowCountApiV1WorkflowCountGet } from "@/client/sdk.gen";
|
||||
import { getServerAccessToken,getServerAuthProvider, getServerUser } from "@/lib/auth/server";
|
||||
import logger from '@/lib/logger';
|
||||
import { getRedirectUrl } from "@/lib/utils";
|
||||
|
|
@ -34,21 +34,18 @@ export default async function AfterSignInPage() {
|
|||
try {
|
||||
const accessToken = await getServerAccessToken();
|
||||
if (accessToken) {
|
||||
const workflowsResponse = await getWorkflowsApiV1WorkflowFetchGet({
|
||||
const countResponse = await getWorkflowCountApiV1WorkflowCountGet({
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
});
|
||||
|
||||
const workflows = workflowsResponse.data ? (Array.isArray(workflowsResponse.data) ? workflowsResponse.data : [workflowsResponse.data]) : [];
|
||||
const activeWorkflows = workflows.filter(w => w.status === 'active');
|
||||
|
||||
logger.debug('[AfterSignInPage] Found workflows:', {
|
||||
total: workflows.length,
|
||||
active: activeWorkflows.length
|
||||
total: countResponse.data?.total,
|
||||
active: countResponse.data?.active
|
||||
});
|
||||
|
||||
if (activeWorkflows.length > 0) {
|
||||
if (countResponse.data && countResponse.data.active > 0) {
|
||||
logger.debug('[AfterSignInPage] Redirecting to /workflow - user has workflows');
|
||||
redirect('/workflow');
|
||||
} else {
|
||||
|
|
|
|||
33
ui/src/app/api/config/version/route.ts
Normal file
33
ui/src/app/api/config/version/route.ts
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import { NextResponse } from "next/server";
|
||||
|
||||
import { healthApiV1HealthGet } from "@/client/sdk.gen";
|
||||
import type { HealthResponse } from "@/client/types.gen";
|
||||
|
||||
// Import version from package.json at build time
|
||||
import packageJson from "../../../../../package.json";
|
||||
|
||||
export async function GET() {
|
||||
const uiVersion = packageJson.version || "dev";
|
||||
|
||||
// Fetch backend version and config from health endpoint
|
||||
let apiVersion = "unknown";
|
||||
let backendApiEndpoint: string | null = null;
|
||||
|
||||
try {
|
||||
const response = await healthApiV1HealthGet();
|
||||
if (response.data) {
|
||||
const data = response.data as HealthResponse;
|
||||
apiVersion = data.version;
|
||||
backendApiEndpoint = data.backend_api_endpoint;
|
||||
}
|
||||
} catch {
|
||||
// Backend might not be reachable during build or in some deployments
|
||||
apiVersion = "unavailable";
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
ui: uiVersion,
|
||||
api: apiVersion,
|
||||
backendApiEndpoint,
|
||||
});
|
||||
}
|
||||
|
|
@ -9,6 +9,7 @@ import AppLayout from "@/components/layout/AppLayout";
|
|||
import PostHogIdentify from "@/components/PostHogIdentify";
|
||||
import SpinLoader from "@/components/SpinLoader";
|
||||
import { Toaster } from "@/components/ui/sonner";
|
||||
import { AppConfigProvider } from "@/context/AppConfigContext";
|
||||
import { OnboardingProvider } from "@/context/OnboardingContext";
|
||||
import { UserConfigProvider } from "@/context/UserConfigContext";
|
||||
import { AuthProvider } from "@/lib/auth";
|
||||
|
|
@ -59,18 +60,20 @@ export default function RootLayout({
|
|||
<body
|
||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}>
|
||||
<AuthProvider>
|
||||
<Suspense fallback={<SpinLoader />}>
|
||||
<UserConfigProvider>
|
||||
<OnboardingProvider>
|
||||
<PostHogIdentify />
|
||||
<AppLayout>
|
||||
{children}
|
||||
</AppLayout>
|
||||
<Toaster />
|
||||
<ChatwootWidget />
|
||||
</OnboardingProvider>
|
||||
</UserConfigProvider>
|
||||
</Suspense>
|
||||
<AppConfigProvider>
|
||||
<Suspense fallback={<SpinLoader />}>
|
||||
<UserConfigProvider>
|
||||
<OnboardingProvider>
|
||||
<PostHogIdentify />
|
||||
<AppLayout>
|
||||
{children}
|
||||
</AppLayout>
|
||||
<Toaster />
|
||||
<ChatwootWidget />
|
||||
</OnboardingProvider>
|
||||
</UserConfigProvider>
|
||||
</Suspense>
|
||||
</AppConfigProvider>
|
||||
</AuthProvider>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { isNextRouterError } from "next/dist/client/components/is-next-router-error";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
import { getWorkflowsApiV1WorkflowFetchGet } from "@/client/sdk.gen";
|
||||
import { getWorkflowCountApiV1WorkflowCountGet } from "@/client/sdk.gen";
|
||||
import SignInClient from "@/components/SignInClient";
|
||||
import { getServerAccessToken,getServerAuthProvider,getServerUser } from "@/lib/auth/server";
|
||||
import logger from '@/lib/logger';
|
||||
|
|
@ -21,21 +21,18 @@ export default async function Home() {
|
|||
try {
|
||||
const accessToken = await getServerAccessToken();
|
||||
if (accessToken) {
|
||||
const workflowsResponse = await getWorkflowsApiV1WorkflowFetchGet({
|
||||
const countResponse = await getWorkflowCountApiV1WorkflowCountGet({
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
});
|
||||
|
||||
const workflows = workflowsResponse.data ? (Array.isArray(workflowsResponse.data) ? workflowsResponse.data : [workflowsResponse.data]) : [];
|
||||
const activeWorkflows = workflows.filter(w => w.status === 'active');
|
||||
|
||||
logger.debug('[HomePage] Found workflows for local provider:', {
|
||||
total: workflows.length,
|
||||
active: activeWorkflows.length
|
||||
total: countResponse.data?.total,
|
||||
active: countResponse.data?.active
|
||||
});
|
||||
|
||||
if (activeWorkflows.length > 0) {
|
||||
if (countResponse.data && countResponse.data.active > 0) {
|
||||
logger.debug('[HomePage] Redirecting to /workflow - user has workflows');
|
||||
redirect('/workflow');
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -326,14 +326,64 @@ export default function UsagePage() {
|
|||
isDisabled={savingTimezone || userConfigLoading}
|
||||
placeholder={userConfigLoading ? "Loading..." : "Select timezone"}
|
||||
styles={{
|
||||
control: (base) => ({
|
||||
control: (base, state) => ({
|
||||
...base,
|
||||
minHeight: '36px',
|
||||
fontSize: '14px',
|
||||
backgroundColor: 'var(--background)',
|
||||
borderColor: state.isFocused ? 'var(--ring)' : 'var(--border)',
|
||||
boxShadow: state.isFocused ? '0 0 0 2px color-mix(in srgb, var(--ring) 20%, transparent)' : 'none',
|
||||
'&:hover': {
|
||||
borderColor: 'var(--border)',
|
||||
},
|
||||
}),
|
||||
menu: (base) => ({
|
||||
...base,
|
||||
zIndex: 9999,
|
||||
backgroundColor: 'var(--popover)',
|
||||
border: '1px solid var(--border)',
|
||||
boxShadow: '0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)',
|
||||
}),
|
||||
menuList: (base) => ({
|
||||
...base,
|
||||
backgroundColor: 'var(--popover)',
|
||||
padding: 0,
|
||||
}),
|
||||
option: (base, state) => ({
|
||||
...base,
|
||||
backgroundColor: state.isSelected
|
||||
? 'var(--accent)'
|
||||
: state.isFocused
|
||||
? 'var(--accent)'
|
||||
: 'var(--popover)',
|
||||
color: 'var(--foreground)',
|
||||
cursor: 'pointer',
|
||||
'&:active': {
|
||||
backgroundColor: 'var(--accent)',
|
||||
},
|
||||
}),
|
||||
singleValue: (base) => ({
|
||||
...base,
|
||||
color: 'var(--foreground)',
|
||||
}),
|
||||
input: (base) => ({
|
||||
...base,
|
||||
color: 'var(--foreground)',
|
||||
}),
|
||||
placeholder: (base) => ({
|
||||
...base,
|
||||
color: 'var(--muted-foreground)',
|
||||
}),
|
||||
indicatorSeparator: (base) => ({
|
||||
...base,
|
||||
backgroundColor: 'var(--border)',
|
||||
}),
|
||||
dropdownIndicator: (base) => ({
|
||||
...base,
|
||||
color: 'var(--muted-foreground)',
|
||||
'&:hover': {
|
||||
color: 'var(--foreground)',
|
||||
},
|
||||
}),
|
||||
}}
|
||||
/>
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -524,6 +524,12 @@ export type HttpValidationError = {
|
|||
detail?: Array<ValidationError>;
|
||||
};
|
||||
|
||||
export type HealthResponse = {
|
||||
status: string;
|
||||
version: string;
|
||||
backend_api_endpoint: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Configuration for HTTP API tools.
|
||||
*/
|
||||
|
|
@ -1042,6 +1048,15 @@ export type VonageConfigurationResponse = {
|
|||
*/
|
||||
export type WebhookCredentialType = 'none' | 'api_key' | 'bearer_token' | 'basic_auth' | 'custom_header';
|
||||
|
||||
/**
|
||||
* Response for workflow count endpoint.
|
||||
*/
|
||||
export type WorkflowCountResponse = {
|
||||
total: number;
|
||||
active: number;
|
||||
archived: number;
|
||||
};
|
||||
|
||||
export type WorkflowError = {
|
||||
kind: ItemKind;
|
||||
id: string | null;
|
||||
|
|
@ -1049,6 +1064,17 @@ export type WorkflowError = {
|
|||
message: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Lightweight response for workflow listings (excludes large fields).
|
||||
*/
|
||||
export type WorkflowListResponse = {
|
||||
id: number;
|
||||
name: string;
|
||||
status: string;
|
||||
created_at: string;
|
||||
total_runs: number;
|
||||
};
|
||||
|
||||
export type WorkflowOption = {
|
||||
id: number;
|
||||
name: string;
|
||||
|
|
@ -1391,6 +1417,7 @@ export type HandleInboundTelephonyApiV1TelephonyInboundWorkflowIdPostData = {
|
|||
'x-twilio-signature'?: string | null;
|
||||
'x-vobiz-signature'?: string | null;
|
||||
'x-vobiz-timestamp'?: string | null;
|
||||
'x-cx-apikey'?: string | null;
|
||||
};
|
||||
path: {
|
||||
workflow_id: number;
|
||||
|
|
@ -1655,6 +1682,39 @@ export type CreateWorkflowFromTemplateApiV1WorkflowCreateTemplatePostResponses =
|
|||
|
||||
export type CreateWorkflowFromTemplateApiV1WorkflowCreateTemplatePostResponse = CreateWorkflowFromTemplateApiV1WorkflowCreateTemplatePostResponses[keyof CreateWorkflowFromTemplateApiV1WorkflowCreateTemplatePostResponses];
|
||||
|
||||
export type GetWorkflowCountApiV1WorkflowCountGetData = {
|
||||
body?: never;
|
||||
headers?: {
|
||||
authorization?: string | null;
|
||||
'X-API-Key'?: string | null;
|
||||
};
|
||||
path?: never;
|
||||
query?: never;
|
||||
url: '/api/v1/workflow/count';
|
||||
};
|
||||
|
||||
export type GetWorkflowCountApiV1WorkflowCountGetErrors = {
|
||||
/**
|
||||
* Not found
|
||||
*/
|
||||
404: unknown;
|
||||
/**
|
||||
* Validation Error
|
||||
*/
|
||||
422: HttpValidationError;
|
||||
};
|
||||
|
||||
export type GetWorkflowCountApiV1WorkflowCountGetError = GetWorkflowCountApiV1WorkflowCountGetErrors[keyof GetWorkflowCountApiV1WorkflowCountGetErrors];
|
||||
|
||||
export type GetWorkflowCountApiV1WorkflowCountGetResponses = {
|
||||
/**
|
||||
* Successful Response
|
||||
*/
|
||||
200: WorkflowCountResponse;
|
||||
};
|
||||
|
||||
export type GetWorkflowCountApiV1WorkflowCountGetResponse = GetWorkflowCountApiV1WorkflowCountGetResponses[keyof GetWorkflowCountApiV1WorkflowCountGetResponses];
|
||||
|
||||
export type GetWorkflowsApiV1WorkflowFetchGetData = {
|
||||
body?: never;
|
||||
headers?: {
|
||||
|
|
@ -1688,7 +1748,7 @@ export type GetWorkflowsApiV1WorkflowFetchGetResponses = {
|
|||
/**
|
||||
* Successful Response
|
||||
*/
|
||||
200: Array<WorkflowResponse>;
|
||||
200: Array<WorkflowListResponse>;
|
||||
};
|
||||
|
||||
export type GetWorkflowsApiV1WorkflowFetchGetResponse = GetWorkflowsApiV1WorkflowFetchGetResponses[keyof GetWorkflowsApiV1WorkflowFetchGetResponses];
|
||||
|
|
@ -4168,6 +4228,41 @@ export type InitiateCallApiV1PublicAgentUuidPostResponses = {
|
|||
|
||||
export type InitiateCallApiV1PublicAgentUuidPostResponse = InitiateCallApiV1PublicAgentUuidPostResponses[keyof InitiateCallApiV1PublicAgentUuidPostResponses];
|
||||
|
||||
export type DownloadWorkflowArtifactApiV1PublicDownloadWorkflowTokenArtifactTypeGetData = {
|
||||
body?: never;
|
||||
path: {
|
||||
token: string;
|
||||
artifact_type: 'recording' | 'transcript';
|
||||
};
|
||||
query?: {
|
||||
/**
|
||||
* Display inline in browser instead of download
|
||||
*/
|
||||
inline?: boolean;
|
||||
};
|
||||
url: '/api/v1/public/download/workflow/{token}/{artifact_type}';
|
||||
};
|
||||
|
||||
export type DownloadWorkflowArtifactApiV1PublicDownloadWorkflowTokenArtifactTypeGetErrors = {
|
||||
/**
|
||||
* Not found
|
||||
*/
|
||||
404: unknown;
|
||||
/**
|
||||
* Validation Error
|
||||
*/
|
||||
422: HttpValidationError;
|
||||
};
|
||||
|
||||
export type DownloadWorkflowArtifactApiV1PublicDownloadWorkflowTokenArtifactTypeGetError = DownloadWorkflowArtifactApiV1PublicDownloadWorkflowTokenArtifactTypeGetErrors[keyof DownloadWorkflowArtifactApiV1PublicDownloadWorkflowTokenArtifactTypeGetErrors];
|
||||
|
||||
export type DownloadWorkflowArtifactApiV1PublicDownloadWorkflowTokenArtifactTypeGetResponses = {
|
||||
/**
|
||||
* Successful Response
|
||||
*/
|
||||
200: unknown;
|
||||
};
|
||||
|
||||
export type DeactivateEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenDeleteData = {
|
||||
body?: never;
|
||||
headers?: {
|
||||
|
|
@ -4500,9 +4595,11 @@ export type HealthApiV1HealthGetResponses = {
|
|||
/**
|
||||
* Successful Response
|
||||
*/
|
||||
200: unknown;
|
||||
200: HealthResponse;
|
||||
};
|
||||
|
||||
export type HealthApiV1HealthGetResponse = HealthApiV1HealthGetResponses[keyof HealthApiV1HealthGetResponses];
|
||||
|
||||
export type ClientOptions = {
|
||||
baseUrl: 'http://127.0.0.1:8000' | (string & {});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ export function MediaPreviewDialog({ accessToken }: MediaPreviewDialogProps) {
|
|||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [mediaType, setMediaType] = useState<'audio' | 'transcript' | null>(null);
|
||||
const [mediaSignedUrl, setMediaSignedUrl] = useState<string | null>(null);
|
||||
const [transcriptContent, setTranscriptContent] = useState<string | null>(null);
|
||||
const [selectedRunId, setSelectedRunId] = useState<number | null>(null);
|
||||
const [mediaDownloadKey, setMediaDownloadKey] = useState<string | null>(null);
|
||||
const [mediaLoading, setMediaLoading] = useState(false);
|
||||
|
|
@ -47,6 +48,7 @@ export function MediaPreviewDialog({ accessToken }: MediaPreviewDialogProps) {
|
|||
async (fileKey: string | null, runId: number) => {
|
||||
if (!fileKey || !accessToken) return;
|
||||
setMediaLoading(true);
|
||||
setTranscriptContent(null);
|
||||
const signed = await getSignedUrl(fileKey, accessToken, true);
|
||||
if (signed) {
|
||||
setMediaType('transcript');
|
||||
|
|
@ -54,6 +56,14 @@ export function MediaPreviewDialog({ accessToken }: MediaPreviewDialogProps) {
|
|||
setMediaDownloadKey(fileKey);
|
||||
setSelectedRunId(runId);
|
||||
setIsOpen(true);
|
||||
// Fetch transcript content with proper UTF-8 encoding
|
||||
try {
|
||||
const response = await fetch(signed);
|
||||
const text = await response.text();
|
||||
setTranscriptContent(text);
|
||||
} catch (error) {
|
||||
console.error('Error fetching transcript:', error);
|
||||
}
|
||||
}
|
||||
setMediaLoading(false);
|
||||
},
|
||||
|
|
@ -84,12 +94,10 @@ export function MediaPreviewDialog({ accessToken }: MediaPreviewDialogProps) {
|
|||
<audio src={mediaSignedUrl} controls autoPlay className="w-full mt-4" />
|
||||
)}
|
||||
|
||||
{!mediaLoading && mediaType === 'transcript' && mediaSignedUrl && (
|
||||
<iframe
|
||||
src={mediaSignedUrl}
|
||||
title="Transcript"
|
||||
className="w-full h-[60vh] border rounded-md mt-4"
|
||||
/>
|
||||
{!mediaLoading && mediaType === 'transcript' && transcriptContent && (
|
||||
<pre className="w-full h-[60vh] overflow-auto border rounded-md mt-4 p-4 bg-muted text-sm whitespace-pre-wrap font-mono">
|
||||
{transcriptContent}
|
||||
</pre>
|
||||
)}
|
||||
|
||||
<DialogFooter className="pt-4">
|
||||
|
|
|
|||
|
|
@ -321,9 +321,20 @@ export default function ServiceConfiguration() {
|
|||
if (!providerSchema) return [];
|
||||
|
||||
// Find all config fields (not provider, not api_key)
|
||||
return Object.keys(providerSchema.properties).filter(
|
||||
const fields = Object.keys(providerSchema.properties).filter(
|
||||
field => field !== "provider" && field !== "api_key"
|
||||
);
|
||||
|
||||
// For Deepgram STT, hide language field when flux-general-en model is selected
|
||||
// Flux model is English-only and doesn't support language selection
|
||||
if (service === "stt" && currentProvider === "deepgram") {
|
||||
const currentModel = watch("stt_model") as string;
|
||||
if (currentModel === "flux-general-en") {
|
||||
return fields.filter(field => field !== "language");
|
||||
}
|
||||
}
|
||||
|
||||
return fields;
|
||||
};
|
||||
|
||||
const renderServiceFields = (service: ServiceSegment) => {
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ interface EndCallNodeProps extends NodeProps {
|
|||
}
|
||||
|
||||
export const EndCall = memo(({ data, selected, id }: EndCallNodeProps) => {
|
||||
const { open, setOpen, handleSaveNodeData } = useNodeHandlers({
|
||||
const { open, setOpen, handleSaveNodeData, handleDeleteNode } = useNodeHandlers({
|
||||
id,
|
||||
additionalData: { is_end: true }
|
||||
});
|
||||
|
|
@ -122,9 +122,14 @@ export const EndCall = memo(({ data, selected, id }: EndCallNodeProps) => {
|
|||
</NodeContent>
|
||||
|
||||
<NodeToolbar isVisible={selected} position={Position.Right}>
|
||||
<Button onClick={() => setOpen(true)} variant="outline" size="icon">
|
||||
<Edit />
|
||||
</Button>
|
||||
<div className="flex flex-col gap-1">
|
||||
<Button onClick={() => setOpen(true)} variant="outline" size="icon">
|
||||
<Edit />
|
||||
</Button>
|
||||
<Button onClick={handleDeleteNode} variant="outline" size="icon">
|
||||
<Trash2Icon />
|
||||
</Button>
|
||||
</div>
|
||||
</NodeToolbar>
|
||||
|
||||
<NodeEditDialog
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import { FlowNodeData } from "@/components/flow/types";
|
|||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { useAppConfig } from "@/context/AppConfigContext";
|
||||
|
||||
import { NodeContent } from "./common/NodeContent";
|
||||
import { NodeEditDialog } from "./common/NodeEditDialog";
|
||||
|
|
@ -26,6 +27,7 @@ interface TriggerNodeProps extends NodeProps {
|
|||
export const TriggerNode = memo(({ data, selected, id }: TriggerNodeProps) => {
|
||||
const { open, setOpen, handleSaveNodeData, handleDeleteNode } = useNodeHandlers({ id });
|
||||
const { saveWorkflow } = useWorkflow();
|
||||
const { config } = useAppConfig();
|
||||
|
||||
// Form state
|
||||
const [name, setName] = useState(data.name || "API Trigger");
|
||||
|
|
@ -33,8 +35,9 @@ export const TriggerNode = memo(({ data, selected, id }: TriggerNodeProps) => {
|
|||
// Generate trigger_path if not present (should be done on node creation)
|
||||
const [triggerPath] = useState(() => data.trigger_path ?? crypto.randomUUID());
|
||||
|
||||
// Get backend URL from environment
|
||||
const backendUrl = process.env.NEXT_PUBLIC_BACKEND_URL || "http://localhost:8000";
|
||||
// Get backend URL from app config (fetched from backend health endpoint)
|
||||
// Falls back to env variable, then to localhost for local development
|
||||
const backendUrl = config?.backendApiEndpoint || process.env.NEXT_PUBLIC_BACKEND_URL || "http://localhost:8000";
|
||||
const endpoint = `${backendUrl}/api/v1/public/agent/${triggerPath}`;
|
||||
|
||||
// Copy state for button feedback
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ import {
|
|||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { useAppConfig } from "@/context/AppConfigContext";
|
||||
import { useAuth } from "@/lib/auth";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
|
|
@ -66,10 +67,14 @@ export function AppSidebar() {
|
|||
const router = useRouter();
|
||||
const { state } = useSidebar();
|
||||
const { provider, getSelectedTeam } = useAuth();
|
||||
const { config } = useAppConfig();
|
||||
|
||||
// Get selected team for Stack auth (cast to Team type from Stack)
|
||||
const selectedTeam = provider === "stack" && getSelectedTeam ? getSelectedTeam() as Team | null : null;
|
||||
|
||||
// Version info from app config context
|
||||
const versionInfo = config ? { ui: config.uiVersion, api: config.apiVersion } : null;
|
||||
|
||||
const isActive = (path: string) => {
|
||||
return pathname.startsWith(path);
|
||||
};
|
||||
|
|
@ -207,6 +212,11 @@ export function AppSidebar() {
|
|||
className="flex items-center gap-2 px-2 text-xl font-bold"
|
||||
>
|
||||
Dograh
|
||||
{versionInfo && (
|
||||
<span className="text-xs font-normal text-muted-foreground">
|
||||
v{versionInfo.ui}
|
||||
</span>
|
||||
)}
|
||||
</Link>
|
||||
)}
|
||||
{/* Toggle button - center it when collapsed */}
|
||||
|
|
@ -445,6 +455,7 @@ export function AppSidebar() {
|
|||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</SidebarFooter>
|
||||
<SidebarRail />
|
||||
|
|
|
|||
58
ui/src/context/AppConfigContext.tsx
Normal file
58
ui/src/context/AppConfigContext.tsx
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
'use client';
|
||||
|
||||
import { createContext, ReactNode, useContext, useEffect, useState } from 'react';
|
||||
|
||||
interface AppConfig {
|
||||
uiVersion: string;
|
||||
apiVersion: string;
|
||||
backendApiEndpoint: string | null;
|
||||
}
|
||||
|
||||
interface AppConfigContextType {
|
||||
config: AppConfig | null;
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
const defaultConfig: AppConfig = {
|
||||
uiVersion: 'dev',
|
||||
apiVersion: 'unknown',
|
||||
backendApiEndpoint: null,
|
||||
};
|
||||
|
||||
const AppConfigContext = createContext<AppConfigContextType>({
|
||||
config: null,
|
||||
loading: true,
|
||||
});
|
||||
|
||||
export function AppConfigProvider({ children }: { children: ReactNode }) {
|
||||
const [config, setConfig] = useState<AppConfig | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
fetch('/api/config/version')
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
setConfig({
|
||||
uiVersion: data.ui || 'dev',
|
||||
apiVersion: data.api || 'unknown',
|
||||
backendApiEndpoint: data.backendApiEndpoint || null,
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
setConfig(defaultConfig);
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<AppConfigContext.Provider value={{ config, loading }}>
|
||||
{children}
|
||||
</AppConfigContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useAppConfig() {
|
||||
return useContext(AppConfigContext);
|
||||
}
|
||||
|
|
@ -2,7 +2,7 @@ import { type ClassValue, clsx } from "clsx"
|
|||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
import { getAuthUserApiV1UserAuthUserGet } from "@/client/sdk.gen";
|
||||
import { getWorkflowsApiV1WorkflowFetchGet } from "@/client/sdk.gen";
|
||||
import { getWorkflowCountApiV1WorkflowCountGet } from "@/client/sdk.gen";
|
||||
import { impersonateApiV1SuperuserImpersonatePost } from "@/client/sdk.gen";
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
|
|
@ -73,21 +73,18 @@ export async function getRedirectUrl(token: string, permissions: { id: string }[
|
|||
// Check if user has any workflows
|
||||
try {
|
||||
console.log('[getRedirectUrl] Checking for existing workflows...');
|
||||
const workflowsResponse = await getWorkflowsApiV1WorkflowFetchGet({
|
||||
const countResponse = await getWorkflowCountApiV1WorkflowCountGet({
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
|
||||
const workflows = workflowsResponse.data ? (Array.isArray(workflowsResponse.data) ? workflowsResponse.data : [workflowsResponse.data]) : [];
|
||||
const activeWorkflows = workflows.filter(w => w.status === 'active');
|
||||
|
||||
console.log('[getRedirectUrl] Found workflows:', {
|
||||
total: workflows.length,
|
||||
active: activeWorkflows.length
|
||||
total: countResponse.data?.total,
|
||||
active: countResponse.data?.active
|
||||
});
|
||||
|
||||
if (activeWorkflows.length > 0) {
|
||||
if (countResponse.data && countResponse.data.active > 0) {
|
||||
console.log('[getRedirectUrl] User has workflows, redirecting to /workflow');
|
||||
return "/workflow";
|
||||
} else {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue