mirror of
https://github.com/dograh-hq/dograh.git
synced 2026-06-10 08:05:22 +02:00
feat: allow recording audio in workflow builder
This commit is contained in:
parent
ac0731a374
commit
2fa4191d9b
22 changed files with 700 additions and 246 deletions
|
|
@ -13,6 +13,7 @@ import { Label } from "@/components/ui/label";
|
|||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { VoiceSelector } from "@/components/VoiceSelector";
|
||||
import { LANGUAGE_DISPLAY_NAMES } from "@/constants/languages";
|
||||
import { useUserConfig } from "@/context/UserConfigContext";
|
||||
|
||||
type ServiceSegment = "llm" | "tts" | "stt" | "embeddings";
|
||||
|
|
@ -46,105 +47,6 @@ const TAB_CONFIG: { key: ServiceSegment; label: string }[] = [
|
|||
{ key: "embeddings", label: "Embedding" },
|
||||
];
|
||||
|
||||
// Display names for language codes (Deepgram + Sarvam)
|
||||
const LANGUAGE_DISPLAY_NAMES: Record<string, string> = {
|
||||
"multi": "Multilingual (Auto-detect)",
|
||||
// Arabic
|
||||
"ar": "Arabic",
|
||||
"ar-AE": "Arabic (UAE)",
|
||||
"ar-SA": "Arabic (Saudi Arabia)",
|
||||
"ar-QA": "Arabic (Qatar)",
|
||||
"ar-KW": "Arabic (Kuwait)",
|
||||
"ar-SY": "Arabic (Syria)",
|
||||
"ar-LB": "Arabic (Lebanon)",
|
||||
"ar-PS": "Arabic (Palestine)",
|
||||
"ar-JO": "Arabic (Jordan)",
|
||||
"ar-EG": "Arabic (Egypt)",
|
||||
"ar-SD": "Arabic (Sudan)",
|
||||
"ar-TD": "Arabic (Chad)",
|
||||
"ar-MA": "Arabic (Morocco)",
|
||||
"ar-DZ": "Arabic (Algeria)",
|
||||
"ar-TN": "Arabic (Tunisia)",
|
||||
"ar-IQ": "Arabic (Iraq)",
|
||||
"ar-IR": "Arabic (Iran)",
|
||||
// Other languages
|
||||
"be": "Belarusian",
|
||||
"bn": "Bengali",
|
||||
"bs": "Bosnian",
|
||||
"bg": "Bulgarian",
|
||||
"ca": "Catalan",
|
||||
"cs": "Czech",
|
||||
"da": "Danish",
|
||||
"da-DK": "Danish (Denmark)",
|
||||
"de": "German",
|
||||
"de-CH": "German (Switzerland)",
|
||||
"el": "Greek",
|
||||
"en": "English",
|
||||
"en-US": "English (US)",
|
||||
"en-AU": "English (Australia)",
|
||||
"en-GB": "English (UK)",
|
||||
"en-IN": "English (India)",
|
||||
"en-NZ": "English (New Zealand)",
|
||||
"es": "Spanish",
|
||||
"es-419": "Spanish (Latin America)",
|
||||
"et": "Estonian",
|
||||
"fa": "Persian",
|
||||
"fi": "Finnish",
|
||||
"fr": "French",
|
||||
"fr-CA": "French (Canada)",
|
||||
"he": "Hebrew",
|
||||
"hi": "Hindi",
|
||||
"hr": "Croatian",
|
||||
"hu": "Hungarian",
|
||||
"id": "Indonesian",
|
||||
"it": "Italian",
|
||||
"ja": "Japanese",
|
||||
"kn": "Kannada",
|
||||
"ko": "Korean",
|
||||
"ko-KR": "Korean (South Korea)",
|
||||
"lt": "Lithuanian",
|
||||
"lv": "Latvian",
|
||||
"mk": "Macedonian",
|
||||
"mr": "Marathi",
|
||||
"ms": "Malay",
|
||||
"nl": "Dutch",
|
||||
"nl-BE": "Flemish",
|
||||
"no": "Norwegian",
|
||||
"pl": "Polish",
|
||||
"pt": "Portuguese",
|
||||
"pt-BR": "Portuguese (Brazil)",
|
||||
"pt-PT": "Portuguese (Portugal)",
|
||||
"ro": "Romanian",
|
||||
"ru": "Russian",
|
||||
"sk": "Slovak",
|
||||
"sl": "Slovenian",
|
||||
"sr": "Serbian",
|
||||
"sv": "Swedish",
|
||||
"sv-SE": "Swedish (Sweden)",
|
||||
"ta": "Tamil",
|
||||
"te": "Telugu",
|
||||
"th": "Thai",
|
||||
"tl": "Tagalog",
|
||||
"tr": "Turkish",
|
||||
"uk": "Ukrainian",
|
||||
"ur": "Urdu",
|
||||
"vi": "Vietnamese",
|
||||
"zh-CN": "Chinese (Simplified)",
|
||||
"zh-TW": "Chinese (Traditional)",
|
||||
// Sarvam Indian languages
|
||||
"bn-IN": "Bengali",
|
||||
"gu-IN": "Gujarati",
|
||||
"hi-IN": "Hindi",
|
||||
"kn-IN": "Kannada",
|
||||
"ml-IN": "Malayalam",
|
||||
"mr-IN": "Marathi",
|
||||
"od-IN": "Odia",
|
||||
"pa-IN": "Punjabi",
|
||||
"ta-IN": "Tamil",
|
||||
"te-IN": "Telugu",
|
||||
"as-IN": "Assamese",
|
||||
};
|
||||
|
||||
// Display names for Sarvam voices
|
||||
const VOICE_DISPLAY_NAMES: Record<string, string> = {
|
||||
"anushka": "Anushka (Female)",
|
||||
|
|
|
|||
|
|
@ -215,11 +215,7 @@ export default function CustomEdge(props: CustomEdgeProps) {
|
|||
const handleSaveEdgeData = useCallback(async (updatedData: FlowEdgeData) => {
|
||||
// Use the workflow store's updateEdge method to properly track history
|
||||
updateEdge(id, { data: updatedData });
|
||||
|
||||
// Save the workflow after updating edge data with a small delay to ensure state is updated
|
||||
setTimeout(async () => {
|
||||
await saveWorkflow();
|
||||
}, 100);
|
||||
await saveWorkflow();
|
||||
}, [id, updateEdge, saveWorkflow]);
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -89,10 +89,7 @@ export const AgentNode = memo(({ data, selected, id }: AgentNodeProps) => {
|
|||
document_uuids: documentUuids.length > 0 ? documentUuids : undefined,
|
||||
});
|
||||
setOpen(false);
|
||||
// Save the workflow after updating node data with a small delay to ensure state is updated
|
||||
setTimeout(async () => {
|
||||
await saveWorkflow();
|
||||
}, 100);
|
||||
await saveWorkflow();
|
||||
};
|
||||
|
||||
// Reset form state when dialog opens
|
||||
|
|
@ -127,27 +124,23 @@ export const AgentNode = memo(({ data, selected, id }: AgentNodeProps) => {
|
|||
}, [data, open]);
|
||||
|
||||
// Handle cleanup of stale document UUIDs
|
||||
const handleStaleDocuments = useCallback((staleUuids: string[]) => {
|
||||
const handleStaleDocuments = useCallback(async (staleUuids: string[]) => {
|
||||
const cleanedUuids = (data.document_uuids ?? []).filter(uuid => !staleUuids.includes(uuid));
|
||||
handleSaveNodeData({
|
||||
...data,
|
||||
document_uuids: cleanedUuids.length > 0 ? cleanedUuids : undefined,
|
||||
});
|
||||
setTimeout(async () => {
|
||||
await saveWorkflow();
|
||||
}, 100);
|
||||
await saveWorkflow();
|
||||
}, [data, handleSaveNodeData, saveWorkflow]);
|
||||
|
||||
// Handle cleanup of stale tool UUIDs
|
||||
const handleStaleTools = useCallback((staleUuids: string[]) => {
|
||||
const handleStaleTools = useCallback(async (staleUuids: string[]) => {
|
||||
const cleanedUuids = (data.tool_uuids ?? []).filter(uuid => !staleUuids.includes(uuid));
|
||||
handleSaveNodeData({
|
||||
...data,
|
||||
tool_uuids: cleanedUuids.length > 0 ? cleanedUuids : undefined,
|
||||
});
|
||||
setTimeout(async () => {
|
||||
await saveWorkflow();
|
||||
}, 100);
|
||||
await saveWorkflow();
|
||||
}, [data, handleSaveNodeData, saveWorkflow]);
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -75,10 +75,7 @@ export const EndCall = memo(({ data, selected, id }: EndCallNodeProps) => {
|
|||
add_global_prompt: addGlobalPrompt,
|
||||
});
|
||||
setOpen(false);
|
||||
// Save the workflow after updating node data with a small delay to ensure state is updated
|
||||
setTimeout(async () => {
|
||||
await saveWorkflow();
|
||||
}, 100);
|
||||
await saveWorkflow();
|
||||
};
|
||||
|
||||
// Reset form state when dialog opens
|
||||
|
|
|
|||
|
|
@ -52,10 +52,7 @@ export const GlobalNode = memo(({ data, selected, id }: GlobalNodeProps) => {
|
|||
name
|
||||
});
|
||||
setOpen(false);
|
||||
// Save the workflow after updating node data with a small delay to ensure state is updated
|
||||
setTimeout(async () => {
|
||||
await saveWorkflow();
|
||||
}, 100);
|
||||
await saveWorkflow();
|
||||
};
|
||||
|
||||
// Reset form state when dialog opens
|
||||
|
|
|
|||
|
|
@ -66,9 +66,7 @@ export const QANode = memo(({ data, selected, id }: QANodeProps) => {
|
|||
qa_sample_rate: qaSampleRate,
|
||||
});
|
||||
setOpen(false);
|
||||
setTimeout(async () => {
|
||||
await saveWorkflow();
|
||||
}, 100);
|
||||
await saveWorkflow();
|
||||
};
|
||||
|
||||
const resetFormState = () => {
|
||||
|
|
|
|||
|
|
@ -104,10 +104,7 @@ export const StartCall = memo(({ data, selected, id }: StartCallNodeProps) => {
|
|||
document_uuids: documentUuids.length > 0 ? documentUuids : undefined,
|
||||
});
|
||||
setOpen(false);
|
||||
// Save the workflow after updating node data with a small delay to ensure state is updated
|
||||
setTimeout(async () => {
|
||||
await saveWorkflow();
|
||||
}, 100);
|
||||
await saveWorkflow();
|
||||
};
|
||||
|
||||
// Reset form state when dialog opens
|
||||
|
|
@ -148,27 +145,23 @@ export const StartCall = memo(({ data, selected, id }: StartCallNodeProps) => {
|
|||
}, [data, open]);
|
||||
|
||||
// Handle cleanup of stale document UUIDs
|
||||
const handleStaleDocuments = useCallback((staleUuids: string[]) => {
|
||||
const handleStaleDocuments = useCallback(async (staleUuids: string[]) => {
|
||||
const cleanedUuids = (data.document_uuids ?? []).filter(uuid => !staleUuids.includes(uuid));
|
||||
handleSaveNodeData({
|
||||
...data,
|
||||
document_uuids: cleanedUuids.length > 0 ? cleanedUuids : undefined,
|
||||
});
|
||||
setTimeout(async () => {
|
||||
await saveWorkflow();
|
||||
}, 100);
|
||||
await saveWorkflow();
|
||||
}, [data, handleSaveNodeData, saveWorkflow]);
|
||||
|
||||
// Handle cleanup of stale tool UUIDs
|
||||
const handleStaleTools = useCallback((staleUuids: string[]) => {
|
||||
const handleStaleTools = useCallback(async (staleUuids: string[]) => {
|
||||
const cleanedUuids = (data.tool_uuids ?? []).filter(uuid => !staleUuids.includes(uuid));
|
||||
handleSaveNodeData({
|
||||
...data,
|
||||
tool_uuids: cleanedUuids.length > 0 ? cleanedUuids : undefined,
|
||||
});
|
||||
setTimeout(async () => {
|
||||
await saveWorkflow();
|
||||
}, 100);
|
||||
await saveWorkflow();
|
||||
}, [data, handleSaveNodeData, saveWorkflow]);
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -61,10 +61,7 @@ export const TriggerNode = memo(({ data, selected, id }: TriggerNodeProps) => {
|
|||
trigger_path: triggerPath,
|
||||
});
|
||||
setOpen(false);
|
||||
// Save the workflow after updating node data
|
||||
setTimeout(async () => {
|
||||
await saveWorkflow();
|
||||
}, 100);
|
||||
await saveWorkflow();
|
||||
};
|
||||
|
||||
// Reset form state when dialog opens
|
||||
|
|
|
|||
|
|
@ -86,9 +86,7 @@ export const WebhookNode = memo(({ data, selected, id }: WebhookNodeProps) => {
|
|||
payload_template: validation.parsed as Record<string, unknown>,
|
||||
});
|
||||
setOpen(false);
|
||||
setTimeout(async () => {
|
||||
await saveWorkflow();
|
||||
}, 100);
|
||||
await saveWorkflow();
|
||||
};
|
||||
|
||||
const handleOpenChange = (newOpen: boolean) => {
|
||||
|
|
|
|||
|
|
@ -71,12 +71,13 @@ export const NodeEditDialog = ({
|
|||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if ((e.metaKey || e.ctrlKey) && e.key === 's') {
|
||||
e.preventDefault();
|
||||
e.stopImmediatePropagation();
|
||||
handleSave();
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||
window.addEventListener('keydown', handleKeyDown, true);
|
||||
return () => window.removeEventListener('keydown', handleKeyDown, true);
|
||||
}, [open, handleSave]);
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -1,22 +1,114 @@
|
|||
'use client';
|
||||
|
||||
import { PlusIcon } from 'lucide-react';
|
||||
import { Bot, ChevronDown, LayoutTemplate, PlusIcon } from 'lucide-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
import { createWorkflowApiV1WorkflowCreateDefinitionPost } from '@/client/sdk.gen';
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { useAuth } from '@/lib/auth';
|
||||
import logger from '@/lib/logger';
|
||||
import { getRandomId } from '@/lib/utils';
|
||||
|
||||
const BLANK_WORKFLOW_DEFINITION = {
|
||||
nodes: [
|
||||
{
|
||||
id: "1",
|
||||
type: "startCall",
|
||||
position: { x: 175, y: 60 },
|
||||
data: {
|
||||
prompt: "# Goal\nYou are a helpful agent who is handing a conversation over voice with a human. This is a voice conversation, so transcripts can be error prone.\n\n## Rules\n- Language: UK English but does not have to be correct english\n- Keep responses short and 2-3 sentences max\n- If you have to repeat something that you said in your previous two turns, then rephrase a bit while keeping the same meaning. Never repeat the exact same words as in your previous 2 responses.\n\n## Speech Handling\n- There could be multiple transcription errors. \n- Accept variations: yes/yeah/yep/aye, no/nah/nope\n- If user says \"sorry?\" or \"pardon me\" or \"can you repeat\" or \"what?\", they might not have heard you- so just repeat what you just said.\n\n### Flow\nStart by saying \"Hi\". Be polite and courteous. ",
|
||||
name: "start call",
|
||||
allow_interrupt: false,
|
||||
invalid: false,
|
||||
validationMessage: null,
|
||||
is_static: false,
|
||||
add_global_prompt: false,
|
||||
wait_for_user_response: false,
|
||||
detect_voicemail: true,
|
||||
delayed_start: false,
|
||||
is_start: true,
|
||||
selected_through_edge: false,
|
||||
hovered_through_edge: false,
|
||||
extraction_enabled: false,
|
||||
selected: false,
|
||||
dragging: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
edges: [],
|
||||
viewport: { x: 808, y: 269, zoom: 0.75 },
|
||||
};
|
||||
|
||||
export function CreateWorkflowButton() {
|
||||
const router = useRouter();
|
||||
const handleClick = () => {
|
||||
const { user, getAccessToken } = useAuth();
|
||||
const [isCreating, setIsCreating] = useState(false);
|
||||
|
||||
const handleAgentBuilder = () => {
|
||||
router.push('/workflow/create');
|
||||
};
|
||||
|
||||
const handleBlankCanvas = async () => {
|
||||
if (isCreating || !user) return;
|
||||
setIsCreating(true);
|
||||
|
||||
try {
|
||||
const accessToken = await getAccessToken();
|
||||
const name = `Workflow-${getRandomId()}`;
|
||||
const response = await createWorkflowApiV1WorkflowCreateDefinitionPost({
|
||||
body: {
|
||||
name,
|
||||
workflow_definition: BLANK_WORKFLOW_DEFINITION as unknown as { [key: string]: unknown },
|
||||
},
|
||||
headers: {
|
||||
'Authorization': `Bearer ${accessToken}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (response.data?.id) {
|
||||
router.push(`/workflow/${response.data.id}`);
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error(`Error creating blank workflow: ${err}`);
|
||||
toast.error('Failed to create workflow');
|
||||
} finally {
|
||||
setIsCreating(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Button
|
||||
onClick={handleClick}
|
||||
>
|
||||
<PlusIcon className="w-4 h-4" />
|
||||
Create Agent
|
||||
</Button>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button disabled={isCreating}>
|
||||
<PlusIcon className="w-4 h-4" />
|
||||
{isCreating ? 'Creating...' : 'Create Agent'}
|
||||
<ChevronDown className="w-4 h-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem onClick={handleAgentBuilder} className="cursor-pointer">
|
||||
<Bot className="w-4 h-4 mr-2" />
|
||||
<div>
|
||||
<div className="font-medium">Use Agent Builder</div>
|
||||
<div className="text-xs text-muted-foreground">AI generates a workflow from your description</div>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={handleBlankCanvas} disabled={isCreating} className="cursor-pointer">
|
||||
<LayoutTemplate className="w-4 h-4 mr-2" />
|
||||
<div>
|
||||
<div className="font-medium">Blank Canvas</div>
|
||||
<div className="text-xs text-muted-foreground">Start from scratch with an empty workflow</div>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue