feat: banner if API is not reachable

This commit is contained in:
Abhishek Kumar 2026-05-31 13:05:22 +05:30
parent ba342b66a7
commit 78ba62e185
15 changed files with 181 additions and 65 deletions

View file

@ -1,4 +1,4 @@
import { Check, ChevronDown, Pause, Play, Search } from "lucide-react";
import { AlertCircle, Check, ChevronDown, Pause, Play, Search } from "lucide-react";
import { useMemo, useState } from "react";
import type { RecordingResponseSchema } from "@/client/types.gen";
@ -10,6 +10,25 @@ import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { useAudioPlayback } from "@/hooks/useAudioPlayback";
import { cn } from "@/lib/utils";
/**
* Amber caveat shown next to free-text fields that are spoken aloud via TTS
* (greetings, transition speech, custom tool messages). Two warnings: the text
* is voiced verbatim (matters for multilingual flows), and realtime
* (speech-to-speech) models have no TTS stage, so static text is never spoken
* a pre-recorded audio file should be used instead.
*/
export function StaticTextWarning() {
return (
<div className="flex items-start gap-2 rounded-md bg-amber-50 p-2 text-xs text-amber-700 border border-amber-200">
<AlertCircle className="h-3.5 w-3.5 mt-0.5 shrink-0" />
<span>
This text is spoken as-is. For multilingual workflows, choose your phrasing carefully.
Realtime (speech-to-speech) models can&apos;t play static text.
</span>
</div>
);
}
interface TextOrAudioInputProps {
type: 'text' | 'audio';
onTypeChange: (type: 'text' | 'audio') => void;

View file

@ -4,7 +4,7 @@ import { useCallback, useEffect, useState } from 'react';
import { useWorkflow, useWorkflowOptional } from "@/app/workflow/[workflowId]/contexts/WorkflowContext";
import { useWorkflowStore } from "@/app/workflow/[workflowId]/stores/workflowStore";
import { TextOrAudioInput } from "@/components/flow/TextOrAudioInput";
import { StaticTextWarning, TextOrAudioInput } from "@/components/flow/TextOrAudioInput";
import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
@ -122,10 +122,7 @@ const EdgeDetailsDialog = ({ open, onOpenChange, data, onSave }: EdgeDetailsDial
recordings={recordings ?? []}
>
<>
<div className="flex items-start gap-2 rounded-md bg-amber-50 p-2 text-xs text-amber-700 border border-amber-200">
<AlertCircle className="h-3.5 w-3.5 mt-0.5 shrink-0" />
<span>This text is spoken as-is. For multilingual workflows, choose your phrasing carefully.</span>
</div>
<StaticTextWarning />
<Textarea
value={transitionSpeech}
placeholder="e.g. Let me transfer you to our billing department..."

View file

@ -1,6 +1,6 @@
"use client";
import { Menu } from "lucide-react";
import { AlertTriangle, Menu, RefreshCw } from "lucide-react";
import Link from "next/link";
import { usePathname } from "next/navigation";
import posthog from "posthog-js";
@ -9,6 +9,7 @@ import React, { ReactNode } from "react";
import { Button } from "@/components/ui/button";
import { SidebarInset, SidebarProvider, useSidebar } from "@/components/ui/sidebar";
import { PostHogEvent } from "@/constants/posthog-events";
import { useAppConfig } from "@/context/AppConfigContext";
import { AppSidebar } from "./AppSidebar";
import { GitHubStarBadge } from "./GitHubStarBadge";
@ -27,7 +28,7 @@ function AppHeader() {
<div className="flex items-center gap-3">
<Button variant="ghost" size="sm" asChild>
<a
href="https://join.slack.com/t/dograh-community/shared_invite/zt-3czr47sw5-MSg1J0kJ7IMPOCHF~03auQ"
href="https://join.slack.com/t/dograh-community/shared_invite/zt-3zjb5vwvl-j7hRz3_F1SOn5cH~jm5f5g"
target="_blank"
rel="noopener noreferrer"
onClick={() => posthog.capture(PostHogEvent.SLACK_COMMUNITY_CLICKED, { source: "app_header" })}
@ -45,6 +46,46 @@ function AppHeader() {
);
}
function BackendStatusBanner() {
const { config, loading, refresh } = useAppConfig();
if (!config || config.backendStatus === "reachable") {
return null;
}
const backendUrl = config.backendUrl && config.backendUrl !== "unknown"
? config.backendUrl
: "the configured backend";
const message = config.backendMessage || `Backend is not reachable at ${backendUrl}.`;
return (
<div
role="alert"
className="border-b border-amber-300 bg-amber-50 px-4 py-3 text-amber-950 dark:border-amber-900/60 dark:bg-amber-950/30 dark:text-amber-100"
>
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div className="flex min-w-0 items-start gap-3">
<AlertTriangle className="mt-0.5 h-5 w-5 shrink-0" />
<div className="min-w-0">
<p className="text-sm font-semibold">Backend connection failed</p>
<p className="break-words text-sm">{message}</p>
</div>
</div>
<Button
variant="outline"
size="sm"
onClick={() => void refresh()}
disabled={loading}
className="h-8 shrink-0 border-amber-400 bg-transparent text-amber-950 hover:bg-amber-100 dark:border-amber-700 dark:text-amber-100 dark:hover:bg-amber-900/40"
>
<RefreshCw className="h-4 w-4" />
Retry
</Button>
</div>
</div>
);
}
interface AppLayoutProps {
children: ReactNode;
headerActions?: ReactNode;
@ -73,6 +114,7 @@ const AppLayout: React.FC<AppLayoutProps> = ({
<div className="flex min-h-screen w-full">
<AppSidebar />
<SidebarInset className="flex-1">
<BackendStatusBanner />
{!isWorkflowEditor && <AppHeader />}
{/* Optional header area for specific pages */}
{headerActions && (
@ -104,6 +146,7 @@ const AppLayout: React.FC<AppLayoutProps> = ({
</div>
) : (
<div className="flex-1 w-full">
<BackendStatusBanner />
{children}
</div>
)}