{_("OpenAI Python SDK Version:")} {openaiVersion.message}
+ )) : null}
+ {(field.value ?? 1).toFixed(2)}
+
+ {formDescription}
++ {_("The following code samples show how to use the bot/agent in a Frappe app or Server Script.")} +
+ ++ {_("Bots can be used to send messages to channels with HTML formatted content.")} +
+
+ {_("You can send markdown formatted text to a channel by setting the")}{" "}
+ markdown{" "}
+ {_("parameter to True.")}
+
+ {_("You can send a message with a link to any document in the system by setting the")}{" "}
+ link_doctype{" "}
+ {_("and")}{" "}
+ link_document{" "}
+ {_("parameters.")}
+
+ {_("You can send a direct message to a user by calling the")}{" "}
+ send_direct_message{" "}
+ {_("method and setting the user_id parameter. This method also accepts markdown and document link parameters.")}
+
+ {sample}
+
+)
+
+export default AgentApiDocsTab
diff --git a/apps/web/src/components/features/settings/panels/Agents/AgentDocumentProcessorsTab.tsx b/apps/web/src/components/features/settings/panels/Agents/AgentDocumentProcessorsTab.tsx
new file mode 100644
index 000000000..d11153551
--- /dev/null
+++ b/apps/web/src/components/features/settings/panels/Agents/AgentDocumentProcessorsTab.tsx
@@ -0,0 +1,204 @@
+import { Controller, useFormContext, useFormState, useWatch } from "react-hook-form"
+import { useFrappeGetCall } from "frappe-react-sdk"
+import { useSetAtom } from "jotai"
+import { CheckIcon, InfoIcon } from "lucide-react"
+import { Alert, AlertDescription } from "@components/ui/alert"
+import { Badge } from "@components/ui/badge"
+import { FormMessage } from "@components/ui/form"
+import { SwitchFormField } from "@components/ui/form-elements"
+import { RadioGroup, RadioGroupItem } from "@components/ui/radio-group"
+import { Separator } from "@components/ui/separator"
+import { Skeleton } from "@components/ui/skeleton"
+import { settingsDialogOpenTab } from "@components/features/settings/settingsDialogAtom"
+import { useRavenSettings } from "@hooks/fetchers/useRavenSettings"
+import type { RavenBot } from "@raven/types/RavenBot/RavenBot"
+import type { ExistingProcessor } from "../DocumentProcessors/DocumentProcessors"
+import _ from "@lib/translate"
+import { isProcessorActive } from "../ai/processorState"
+
+interface ExistingProcessorsResponse {
+ message: ExistingProcessor[]
+}
+
+const getProcessorTypeBestFor = (processorType: string) => {
+ switch (processorType) {
+ case "OCR_PROCESSOR":
+ return ["General text extraction", "Multi-language documents", "Handwriting recognition"]
+ case "FORM_PARSER_PROCESSOR":
+ return ["Application forms", "Surveys", "Registration forms", "Structured data"]
+ case "BANK_STATEMENT_PROCESSOR":
+ return ["Financial analysis", "Loan processing", "Bank statement digitization"]
+ case "INVOICE_PROCESSOR":
+ return ["Invoice processing", "Accounts payable automation", "Tax document preparation", "Financial record keeping"]
+ case "EXPENSE_PROCESSOR":
+ return ["Expense reports", "Travel reimbursements", "Receipt digitization"]
+ default:
+ return []
+ }
+}
+
+/** Document Processors tab of the Raven Bot editor. */
+const AgentDocumentProcessorsTab = () => {
+ const { control } = useFormContext+ {_("Choose an existing document processor for this bot. Processors can be shared across multiple bots.")} +
+ ++ {_( + "This processor will be used to process any documents, images, or PDFs uploaded to the thread and send its results to the agent for better context." + )} +
+ + {formState.errors.google_document_processor_id && ( ++ {_("Files like manuals, sheets etc can be added to the AI agent as instructions.")} +
++ {_("No files attached yet. Upload a file or select one from your file sources.")} +
+ ) : ( ++ {_("No more file sources to add.")} +
+ ) : ( +
+ {_("Add functions that the bot can use to create or update documents in the system.")}
+
+ {_("Create functions in the")}{" "}
+ {" "}
+ {_("and then add them here.")}
+
{field.description}
+{String(errors.prompt.message)}
} ++ {_("No processors created yet. Select a type below to create your first processor.")} +
+ ) : ( + <> +{field.description}
+{_("Default Value:")} {field.default_value}
: null} + {field.default_value && field.child_table_name ?{_("Child Table:")} {options} ({field.child_table_name})
+ ) : null} +
+ {_("You can limit the values that the bot can fill in this field by adding options.")}
+
+ {_("This helps the bot to make less mistakes.")}
+
{item.value}
+ {item.description ?{item.description}
: null} ++ {isDocRef + ? _("Select a Reference Doctype on the Details tab to define variables.") + : _("Pick a Create/Update document type or Custom Function on the Details tab to define variables.")} +
+ ) +} + +export default FunctionForm diff --git a/apps/web/src/components/features/settings/panels/Functions/FunctionListView.tsx b/apps/web/src/components/features/settings/panels/Functions/FunctionListView.tsx new file mode 100644 index 000000000..c64db4899 --- /dev/null +++ b/apps/web/src/components/features/settings/panels/Functions/FunctionListView.tsx @@ -0,0 +1,153 @@ +import { useMemo } from "react" +import { useFrappeGetDocList } from "frappe-react-sdk" +import type { ColumnDef } from "@tanstack/react-table" +import { Badge } from "@components/ui/badge" +import { Button } from "@components/ui/button" +import { Empty, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from "@components/ui/empty" +import ErrorBanner from "@components/ui/error-banner" +import { ListView, type ListViewColumnMeta } from "@components/ui/list-view" +import { + SettingsPanelContent, + SettingsPanelDescription, + SettingsPanelHeader, + SettingsPanelTitle, +} from "@components/ui/settings-dialog" +import { Spinner } from "@components/ui/spinner" +import { TablePagination } from "@components/ui/table-pagination" +import usePaginatedList from "@hooks/usePaginatedList" +import { CheckIcon, SquareFunctionIcon } from "lucide-react" +import { isRavenSettingsAdmin } from "../AdminSettingsForm" +import type { RavenAIFunction } from "@raven/types/RavenAI/RavenAIFunction" +import AINotEnabledCallout from "../ai/AINotEnabledCallout" +import _ from "@lib/translate" + +export const FUNCTIONS_LIST_KEY = "raven-ai-functions" + +/** AI → Functions: list of declared functions. Non-admins only see the empty state. */ +const FunctionListView = ({ onOpen, onCreate }: { onOpen: (id: string) => void; onCreate: () => void }) => { + const isAdmin = isRavenSettingsAdmin() + const pagination = usePaginatedList(FUNCTIONS_LIST_KEY, "Raven AI Function", isAdmin) + + const { data, error } = useFrappeGetDocList{error}
} +{_("Error loading variables in the builder. Please check your JSON structure.")}
+ } + } + const value = typeof field.value === "string" ? field.value : field.value ? JSON.stringify(field.value, null, 4) : "" + return ( +{_("No variables defined")}
+ )} + {Object.entries(properties).map(([key, value]) => ( +{value.description}
+ {(value.type === "string" || value.type === "number") && value.enum && value.enum.length > 0 && ( + {_("Options:")} {value.enum.join(", ")} + )} +{_("Variable name must be unique, and cannot have spaces.")}
+{"{{user}}"}
+ {_("\".")}
+
+ {_("Here are some variables you can use in your instruction. Simply copy by clicking on the variable.")}
+
+ {_("You can also use standard Jinja variables available in the system.")}
+
{ if (e.key === "Enter" || e.key === " ") { e.preventDefault(); copyText() } }} aria-label={_("Copy to clipboard")} className="cursor-pointer rounded bg-surface-gray-2 px-1.5 py-0.5 text-base md:text-sm">
+ {text}
+
+ {data?.message}
+ :{_("Nothing to preview")}
} +{_("No templates found")}
} +
diff --git a/apps/web/src/components/ui/select.tsx b/apps/web/src/components/ui/select.tsx
index 527b86901..0e9e17db3 100644
--- a/apps/web/src/components/ui/select.tsx
+++ b/apps/web/src/components/ui/select.tsx
@@ -139,7 +139,7 @@ function SelectItem({
data-slot="select-item"
className={cn(
"focus-visible:outline-none select-none relative flex w-full cursor-pointer items-center gap-2 rounded py-1.5 pe-8 px-2",
- "focus:bg-surface-gray-2 text-ink-gray-7 [&_svg:not([class*='text-'])]:text-ink-gray-6 text-base [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
+ "focus:bg-surface-gray-2 text-ink-gray-7 [&_svg:not([class*='text-'])]:text-ink-gray-6 text-base [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2 *:[span]:last:min-w-0",
"data-disabled:pointer-events-none data-disabled:cursor-not-allowed data-disabled:text-ink-gray-4",
className
)}
diff --git a/apps/web/src/components/ui/settings-dialog.tsx b/apps/web/src/components/ui/settings-dialog.tsx
index d2be1f195..849ca9993 100644
--- a/apps/web/src/components/ui/settings-dialog.tsx
+++ b/apps/web/src/components/ui/settings-dialog.tsx
@@ -54,13 +54,13 @@ function SettingsDialog({
const contextValue = React.useMemo(() => ({ onClose }), [onClose])
return (
-
+ {/* tabular-nums: digits keep one width, so page flips don't shift the row. */}
+
{_("Showing {0}-{1} of {2}", [String(start), String(end), String(totalCount)])}