mirror of
https://github.com/dograh-hq/dograh.git
synced 2026-06-07 07:55:16 +02:00
feat: add support for self hosted llm models
This commit is contained in:
parent
31e075d114
commit
ac0731a374
17 changed files with 179 additions and 48 deletions
|
|
@ -48,6 +48,12 @@ new api route in backend, and wish to use it in the UI, generate the client usin
|
|||
npm run generate-client
|
||||
```
|
||||
|
||||
## Conventions
|
||||
|
||||
### File Uploads
|
||||
|
||||
Always use a hidden `<input type="file">` with a visible `<Button>` that triggers it via `fileInputRef.current?.click()`. Never use a visible `<Input type="file">` — the native file input styling is inconsistent and confusing. Show the selected filename next to or below the button.
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
|
|
|
|||
|
|
@ -519,13 +519,17 @@ export default function RunsPage() {
|
|||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() => {
|
||||
const filter = encodeURIComponent(
|
||||
`metadata;stringObject;attributes;contains;conversation.id,metadata;stringObject;attributes;contains;${run.id}`,
|
||||
);
|
||||
window.open(
|
||||
`${process.env.NEXT_PUBLIC_LANGFUSE_ENDPOINT}/project/${process.env.NEXT_PUBLIC_LANGFUSE_PROJECT_ID}/traces?search=&filter=${filter}&dateRange=All+time`,
|
||||
'_blank',
|
||||
);
|
||||
if (run.gathered_context?.trace_url) {
|
||||
window.open(String(run.gathered_context.trace_url), '_blank');
|
||||
} else {
|
||||
const filter = encodeURIComponent(
|
||||
`metadata;stringObject;attributes;contains;conversation.id,metadata;stringObject;attributes;contains;${run.id}`,
|
||||
);
|
||||
window.open(
|
||||
`${process.env.NEXT_PUBLIC_LANGFUSE_ENDPOINT}/project/${process.env.NEXT_PUBLIC_LANGFUSE_PROJECT_ID}/traces?search=&filter=${filter}&dateRange=All+time`,
|
||||
'_blank',
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
|
|
|
|||
|
|
@ -216,7 +216,7 @@ export const RecordingsDialog = ({
|
|||
<Label className="text-xs text-muted-foreground">
|
||||
Audio File
|
||||
</Label>
|
||||
<Input
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept="audio/*"
|
||||
|
|
@ -233,11 +233,24 @@ export const RecordingsDialog = ({
|
|||
setError(null);
|
||||
setSelectedFile(file);
|
||||
}}
|
||||
className="text-sm"
|
||||
className="hidden"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
Max 5MB
|
||||
</p>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="w-full justify-start text-sm font-normal"
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
>
|
||||
<Upload className="w-4 h-4 mr-2 shrink-0" />
|
||||
{selectedFile ? (
|
||||
<span className="truncate">
|
||||
{selectedFile.name} ({(selectedFile.size / (1024 * 1024)).toFixed(1)}MB)
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-muted-foreground">Choose audio file (max 5MB)</span>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-xs text-muted-foreground">
|
||||
|
|
@ -289,8 +302,8 @@ export const RecordingsDialog = ({
|
|||
>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<code className="text-xs bg-muted px-1.5 py-0.5 rounded font-mono">
|
||||
{rec.recording_id}
|
||||
<code className="text-xs bg-muted px-1.5 py-0.5 rounded font-mono truncate max-w-[300px]">
|
||||
{(rec.metadata?.original_filename as string) || rec.recording_id}
|
||||
</code>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground mt-1 break-all line-clamp-2">
|
||||
|
|
|
|||
|
|
@ -18,14 +18,12 @@ export const layoutNodes = (
|
|||
// Separate nodes by type
|
||||
const triggerNodes = nodes.filter(n => n.type === NodeType.TRIGGER);
|
||||
const webhookNodes = nodes.filter(n => n.type === NodeType.WEBHOOK);
|
||||
const globalNodes = nodes.filter(n => n.type === NodeType.GLOBAL_NODE || n.type === 'global');
|
||||
const qaNodes = nodes.filter(n => n.type === NodeType.QA);
|
||||
const globalNodes = nodes.filter(n => n.type === NodeType.GLOBAL_NODE);
|
||||
const workflowNodes = nodes.filter(n =>
|
||||
n.type === NodeType.START_CALL ||
|
||||
n.type === NodeType.AGENT_NODE ||
|
||||
n.type === NodeType.END_CALL ||
|
||||
n.type === 'startCall' ||
|
||||
n.type === 'agentNode' ||
|
||||
n.type === 'endCall'
|
||||
n.type === NodeType.END_CALL
|
||||
);
|
||||
|
||||
// If no workflow nodes, just return original nodes
|
||||
|
|
@ -161,12 +159,26 @@ export const layoutNodes = (
|
|||
};
|
||||
});
|
||||
|
||||
// Position QA nodes below webhook nodes on the right side
|
||||
const qaStartY = webhookNodes.length > 0
|
||||
? workflowCenterY - (webhookNodes.length * NODE_HEIGHT + (webhookNodes.length - 1) * VERTICAL_SPACING) / 2
|
||||
+ webhookNodes.length * (NODE_HEIGHT + VERTICAL_SPACING) + VERTICAL_SPACING
|
||||
: workflowCenterY;
|
||||
const positionedQaNodes = qaNodes.map((node, index) => ({
|
||||
...node,
|
||||
position: {
|
||||
x: webhookNodesX,
|
||||
y: qaStartY + index * (NODE_HEIGHT + VERTICAL_SPACING)
|
||||
}
|
||||
}));
|
||||
|
||||
// Combine all positioned nodes
|
||||
const allPositionedNodes = [
|
||||
...positionedTriggerNodes,
|
||||
...positionedGlobalNodes,
|
||||
...positionedWorkflowNodes,
|
||||
...positionedWebhookNodes
|
||||
...positionedWebhookNodes,
|
||||
...positionedQaNodes
|
||||
];
|
||||
|
||||
// Create a map for quick lookup
|
||||
|
|
|
|||
|
|
@ -236,11 +236,21 @@ export default function ServiceConfiguration() {
|
|||
}
|
||||
});
|
||||
selectedProviders[service] = userConfig?.[service]?.provider as string;
|
||||
// Fill in schema defaults for fields not present in userConfig
|
||||
const properties = response.data[service]?.[selectedProviders[service]]?.properties as Record<string, SchemaProperty>;
|
||||
if (properties) {
|
||||
Object.entries(properties).forEach(([field, schema]) => {
|
||||
const key = `${service}_${field}`;
|
||||
if (field !== "provider" && field !== "api_key" && schema.default !== undefined && !(key in defaultValues)) {
|
||||
defaultValues[key] = schema.default;
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const properties = response.data[service]?.[selectedProviders[service]]?.properties as Record<string, SchemaProperty>;
|
||||
if (properties) {
|
||||
Object.entries(properties).forEach(([field, schema]) => {
|
||||
if (field !== "provider" && schema.default) {
|
||||
if (field !== "provider" && schema.default !== undefined) {
|
||||
defaultValues[`${service}_${field}`] = schema.default;
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ export interface MentionItem {
|
|||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
filename: string;
|
||||
}
|
||||
|
||||
interface MentionTextareaProps {
|
||||
|
|
@ -46,6 +47,7 @@ export function MentionTextarea({
|
|||
id: r.recording_id,
|
||||
name: r.transcript,
|
||||
description: r.transcript,
|
||||
filename: (r.metadata?.original_filename as string) || r.recording_id,
|
||||
})),
|
||||
[recordings]
|
||||
);
|
||||
|
|
@ -195,7 +197,7 @@ export function MentionTextarea({
|
|||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<code className="text-xs bg-muted px-1 py-0.5 rounded font-mono">
|
||||
{item.id}
|
||||
{item.filename}
|
||||
</code>
|
||||
<span className="font-medium truncate">{item.name}</span>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue