File creation agent skills (#5280)

* Powerpoint File Creation (#5278)

* wip

* download card

* UI for downloading

* move to fs system with endpoint to pull files

* refactor UI

* final-pass

* remove save-file-browser skill and refactor

* remove fileDownload event

* reset

* reset file

* reset timeout

* persist toggle

* Txt creation (#5279)

* wip

* download card

* UI for downloading

* move to fs system with endpoint to pull files

* refactor UI

* final-pass

* remove save-file-browser skill and refactor

* remove fileDownload event

* reset

* reset file

* reset timeout

* wip

* persist toggle

* add arbitrary text creation file

* Add PDF document generation with markdown formatting (#5283)

add support for branding in bottom right corner
refactor core utils and frontend rendering

* Xlsx document creation (#5284)

add Excel doc & sheet creation

* Basic docx creation (#5285)

* Basic docx creation

* add test theme support + styling and title pages

* simplify skill selection

* handle TG attachments

* send documents over tg

* lazy import

* pin deps

* fix lock

* i18n for file creation (#5286)

i18n for file-creation
connect #5280

* theme overhaul

* Add PPTX subagent for better results

* forgot files

* Add PPTX subagent for better results (#5287)

* Add PPTX subagent for better results

* forgot files

* make sub-agent use proper tool calling if it can and better UI hints
This commit is contained in:
Timothy Carambat
2026-03-30 15:13:39 -07:00
committed by GitHub
parent 0bfd27c6df
commit 7aaea7f514
70 changed files with 7349 additions and 932 deletions

View File

@@ -0,0 +1,126 @@
import { memo, useState } from "react";
import { saveAs } from "file-saver";
import { DownloadSimple, CircleNotch } from "@phosphor-icons/react";
import { humanFileSize } from "@/utils/numbers";
import StorageFiles from "@/models/files";
/**
* @param {{content: {filename: string, storageFilename?: string, fileSize?: number}}} props
*/
function FileDownloadCard({ props }) {
const { filename, storageFilename, fileSize } = props.content || {};
const { badge, badgeBg, badgeText, fileType } = getFileDisplayInfo(filename);
const [downloading, setDownloading] = useState(false);
const handleDownload = async () => {
if (downloading) return;
if (!storageFilename) return;
setDownloading(true);
try {
const blob = await StorageFiles.download(storageFilename);
if (!blob) throw new Error("Failed to download file");
saveAs(blob, filename || storageFilename);
} catch {
console.error("Failed to download file");
} finally {
setDownloading(false);
}
};
return (
<div className="flex justify-center w-full my-2">
<div className="w-full max-w-[750px] mr-4">
<div className="flex items-center justify-between bg-zinc-800 light:bg-slate-100 light:border light:border-slate-200/50 rounded-xl px-2 py-1">
<div className="flex items-center gap-x-3 min-w-0">
<div
className={`${badgeBg} ${badgeText} rounded-lg flex items-center justify-center flex-shrink-0 h-[48px] w-[48px] text-xs font-bold`}
>
{badge}
</div>
<div className="flex flex-col min-w-0">
<p className="text-white light:text-slate-900 text-sm font-medium truncate leading-snug">
{filename || "Unknown file"}
</p>
<p className="text-zinc-400 light:text-slate-500 text-xs leading-snug">
{humanFileSize(fileSize, true, 1)}
{fileSize && fileType ? " · " : ""}
{fileType}
</p>
</div>
</div>
<button
onClick={handleDownload}
disabled={downloading}
className="flex items-center gap-x-2 px-4 py-2 rounded-lg border border-zinc-600 light:border-theme-sidebar-border hover:bg-zinc-700 light:hover:bg-theme-bg-secondary transition-colors text-white light:text-theme-text-primary text-sm font-medium flex-shrink-0 ml-4 disabled:opacity-50 disabled:cursor-not-allowed"
>
{downloading ? (
<CircleNotch size={16} weight="bold" className="animate-spin" />
) : (
<DownloadSimple size={16} weight="bold" />
)}
<span>{downloading ? "Downloading..." : "Download"}</span>
</button>
</div>
</div>
</div>
);
}
/**
* Get display info for a file based on its extension
* @param {string} filename
* @returns {{badge: string, badgeBg: string, badgeText: string, fileType: string}}
*/
function getFileDisplayInfo(filename) {
const extension = filename?.split(".")?.pop()?.toLowerCase() ?? "txt";
switch (extension) {
case "pptx":
case "ppt":
return {
badge: "PPT",
badgeBg: "bg-orange-100",
badgeText: "text-orange-700",
fileType: "PowerPoint",
};
case "pdf":
return {
badge: "PDF",
badgeBg: "bg-red-100",
badgeText: "text-red-700",
fileType: "PDF Document",
};
case "doc":
case "docx":
return {
badge: "DOC",
badgeBg: "bg-blue-100",
badgeText: "text-blue-700",
fileType: "Word Document",
};
case "xls":
case "xlsx":
return {
badge: "XLS",
badgeBg: "bg-green-100",
badgeText: "text-green-700",
fileType: "Spreadsheet",
};
case "csv":
return {
badge: "CSV",
badgeBg: "bg-green-100",
badgeText: "text-green-700",
fileType: "Spreadsheet",
};
default:
return {
badge: extension.toUpperCase().slice(0, 4),
badgeBg: "bg-slate-200",
badgeText: "text-slate-700",
fileType: "File",
};
}
}
export default memo(FileDownloadCard);

View File

@@ -0,0 +1,19 @@
import { memo } from "react";
import FileDownloadCard from "../../FileDownloadCard";
function HistoricalOutputs({ outputs = [] }) {
if (!outputs || outputs.length === 0) return null;
return (
<div className="flex flex-col gap-2 mt-4">
{outputs.map((output, index) => (
<FileDownloadCard
key={`${output.type}-${index}`}
props={{ content: output.payload }}
/>
))}
</div>
);
}
export default memo(HistoricalOutputs);

View File

@@ -18,6 +18,7 @@ import paths from "@/utils/paths";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import { chatQueryRefusalResponse } from "@/utils/chat";
import HistoricalOutputs from "./HistoricalOutputs";
const HistoricalMessage = ({
uuid = v4(),
@@ -34,6 +35,7 @@ const HistoricalMessage = ({
saveEditedMessage,
forkThread,
metrics = {},
outputs = [],
}) => {
const { t } = useTranslation();
const { isEditing } = useEditMessage({ chatId, role });
@@ -156,6 +158,7 @@ const HistoricalMessage = ({
</Link>
)}
<ChatAttachments attachments={attachments} />
<HistoricalOutputs outputs={outputs} />
</div>
)}
<div className="flex items-start md:items-center gap-x-1">

View File

@@ -10,6 +10,7 @@ import HistoricalMessage from "./HistoricalMessage";
import PromptReply from "./PromptReply";
import StatusResponse from "./StatusResponse";
import ToolApprovalRequest from "./ToolApprovalRequest";
import FileDownloadCard from "./FileDownloadCard";
import { useManageWorkspaceModal } from "../../../Modals/ManageWorkspace";
import ManageWorkspace from "../../../Modals/ManageWorkspace";
import { ArrowDown } from "@phosphor-icons/react";
@@ -307,6 +308,8 @@ function buildMessages({
if (props.type === "rechartVisualize" && !!props.content) {
acc.push(<Chartable key={props.uuid} props={props} />);
} else if (props.type === "fileDownloadCard" && !!props.content) {
acc.push(<FileDownloadCard key={props.uuid} props={props} />);
} else if (isLastBotReply && props.animate) {
acc.push(
<PromptReply
@@ -337,6 +340,7 @@ function buildMessages({
saveEditedMessage={saveEditedMessage}
forkThread={forkThread}
metrics={props.metrics}
outputs={props.outputs}
/>
);
}

View File

@@ -2,7 +2,6 @@
const TRANSLATIONS = {
onboarding: {
home: {
title: "مرحبا في",
getStarted: "بسم الله",
welcome: "أهلاً وسهلاً",
},
@@ -308,11 +307,6 @@ const TRANSLATIONS = {
description:
"تمكين الوكيل الافتراضي لإنشاء أنواع مختلفة من المخططات من البيانات المقدمة أو المعطاة في المحادثة.",
},
save: {
title: "إنشاء الملفات وحفظها في المتصفح",
description:
"تمكين الوكيل الافتراضي من إنشاء الملفات والكتابة عليها وحفظها و تنزيلها في متصفحك.",
},
web: {
title: "البحث والتصفح المباشر على الويب",
description:
@@ -357,10 +351,6 @@ const TRANSLATIONS = {
title: "الحصول على معلومات الملف",
description: "احصل على بيانات وصف تفصيلية حول الملفات.",
},
"write-file": {
title: "إنشاء ملف",
description: "إنشاء ملفات جديدة أو استبدال الملفات الموجودة.",
},
"edit-file": {
title: "تحرير الملف",
description:
@@ -378,6 +368,43 @@ const TRANSLATIONS = {
title: "نسخ الملف",
description: "نسخ الملفات والمجلدات",
},
"write-text-file": {
title: "إنشاء ملف نصي",
description:
"إنشاء ملفات نصية جديدة أو استبدال الملفات النصية الموجودة.",
},
},
},
createFiles: {
title: "إنشاء المستندات",
description:
"اسمح لمسؤولك بإنشاء ملفات بتنسيقات ثنائية مثل عروض PowerPoint وجداول Excel ووثائق Word وملفات PDF. يمكن تنزيل الملفات مباشرة من نافذة الدردشة.",
configuration: "أنواع المستندات المتاحة",
skills: {
"create-text-file": {
title: "ملفات النصوص",
description:
"إنشاء ملفات نصية تحتوي على أي محتوى وتنسيق (مثل .txt، .md، .json، .csv، إلخ).",
},
"create-pptx": {
title: "عروض تقديمية باستخدام برنامج باوربوينت",
description:
"إنشاء عروض تقديمية جديدة باستخدام برنامج باوربوينت، تتضمن الشرائح والعناوين والنقاط.",
},
"create-pdf": {
title: "ملفات PDF",
description:
"إنشاء مستندات PDF من ملفات Markdown أو النصوص البسيطة مع تنسيق أساسي.",
},
"create-xlsx": {
title: "جداول بيانات في برنامج إكسل",
description:
"إنشاء مستندات Excel للبيانات الجدولية مع الأوراق والتصميم.",
},
"create-docx": {
title: "ملفات مستندات مايكروسوفت",
description: "إنشاء مستندات Word مع تنسيق وتصميم أساسي.",
},
},
},
},
@@ -694,7 +721,6 @@ const TRANSLATIONS = {
select_all: "حدد الكل",
deselect_all: "إلغاء التحديد الكل",
remove_selected: "حذف المحدد",
costs: "*تكلفة ثابتة لإنشاء التمثيلات",
save_embed: "حفظ و تضمين",
"total-documents_one": "{{count}}",
"total-documents_other": "{{count}} المستندات",

View File

@@ -2,7 +2,6 @@
const TRANSLATIONS = {
onboarding: {
home: {
title: "Vítejte v",
getStarted: "Začít",
welcome: "Vítejte",
},
@@ -324,11 +323,6 @@ const TRANSLATIONS = {
description:
"Umožněte výchozímu agentovi generovat různé typy grafů z dat poskytnutých nebo uvedených v chatu.",
},
save: {
title: "Generovat a ukládat soubory",
description:
"Umožněte výchozímu agentovi generovat a zapisovat do souborů, které lze uložit do počítače.",
},
web: {
title: "Živé webové vyhledávání a prohlížení",
description:
@@ -373,11 +367,6 @@ const TRANSLATIONS = {
title: "Získejte informace o souboru",
description: "Získejte podrobné metadatumy o souborech.",
},
"write-file": {
title: "Vytvoř soubor",
description:
"Vytvořte nové soubory nebo přepsat stávající soubory.",
},
"edit-file": {
title: "Upravit soubor",
description:
@@ -395,6 +384,44 @@ const TRANSLATIONS = {
title: "Zkopírovat soubor",
description: "Zkopírujte soubory a adresáře",
},
"write-text-file": {
title: "Vytvořte soubor s textem",
description:
"Vytvořte nové textové soubory nebo přepsáním existujících textových souborů.",
},
},
},
createFiles: {
title: "Vytváření dokumentů",
description:
"Umožněte svému zástupci vytvářet soubory ve formátech jako PowerPoint prezentace, tabulky v Excelu, dokumenty ve formátu Word a soubory ve formátu PDF. Soubory lze stahovat přímo z chatu.",
configuration: "Dostupné typy dokumentů",
skills: {
"create-text-file": {
title: "Soubory v textovém formátu",
description:
"Vytvořte textové soubory s libovolným obsahem a příponou (např. .txt, .md, .json, .csv atd.)",
},
"create-pptx": {
title: "Prezentace v PowerPointu",
description:
"Vytvořte nové prezentace v programu PowerPoint, včetně slidů, nadpisů a odrážek.",
},
"create-pdf": {
title: "Dokumenty ve formátu PDF",
description:
"Vytvořte PDF dokumenty z Markdownu nebo jednoduchého textu s základním formátováním.",
},
"create-xlsx": {
title: "Tabulky v programu Excel",
description:
"Vytvořte tabulkové dokumenty v programu Excel, které budou obsahovat listy a stylování.",
},
"create-docx": {
title: "Dokumenty ve formátu Word",
description:
"Vytvořte dokumenty ve formátu Word s základním formátováním a stylováním.",
},
},
},
},
@@ -833,7 +860,6 @@ const TRANSLATIONS = {
select_all: "Vybrat vše",
deselect_all: "Zrušit výběr všeho",
remove_selected: "Odebrat vybrané",
costs: "*Jednorázové náklady pro embeddingy",
save_embed: "Uložit a vložit",
"total-documents_one": "{{count}} dokument",
"total-documents_other": "{{count}} dokumenty",

View File

@@ -2,7 +2,6 @@
const TRANSLATIONS = {
onboarding: {
home: {
title: "Velkommen til",
getStarted: "Kom godt i gang",
welcome: "Velkommen",
},
@@ -312,11 +311,6 @@ const TRANSLATIONS = {
description:
"Gør det muligt for standardagenten at generere forskellige typer diagrammer fra data, der leveres eller gives i chat.",
},
save: {
title: "Generer og gem filer i browseren",
description:
"Gør det muligt for standardagenten at generere og skrive til filer, der gemmes og kan downloades i din browser.",
},
web: {
title: "Live web-søgning og browsing",
description:
@@ -361,10 +355,6 @@ const TRANSLATIONS = {
title: "Få filinformation",
description: "Få detaljerede metadata om filer",
},
"write-file": {
title: "Opret fil",
description: "Opret nye filer eller skriv over eksisterende filer.",
},
"edit-file": {
title: "Rediger fil",
description: "Rediger tekstfiler baseret på linjer",
@@ -381,6 +371,44 @@ const TRANSLATIONS = {
title: "Kopier fil",
description: "Kopier filer og mapper",
},
"write-text-file": {
title: "Opret tekstfil",
description:
"Opret nye tekstfiler eller overskriv eksisterende tekstfiler.",
},
},
},
createFiles: {
title: "Dokumentoprettelse",
description:
"Giv din agent mulighed for at oprette binære dokumentformater som PowerPoint-præsentationer, Excel-regneark, Word-dokumenter og PDF-filer. Filerne kan downloades direkte fra chatvinduet.",
configuration: "Tilgængelige dokumenttyper",
skills: {
"create-text-file": {
title: "Tekstfiler",
description:
"Opret tekstfiler med enhver indhold og filtype (.txt, .md, .json, .csv osv.)",
},
"create-pptx": {
title: "PowerPoint-præsentationer",
description:
"Opret nye PowerPoint-præsentationer med slides, overskrifter og punktlister.",
},
"create-pdf": {
title: "PDF-dokumenter",
description:
"Opret PDF-dokumenter fra Markdown eller almindelig tekst med grundlæggende formatering.",
},
"create-xlsx": {
title: "Excel-regneark",
description:
"Opret Excel-dokumenter med tabellerede data, inklusive ark og formatering.",
},
"create-docx": {
title: "Ord-dokumenter",
description:
"Opret Word-dokumenter med grundlæggende formatering og stil.",
},
},
},
},
@@ -700,7 +728,6 @@ const TRANSLATIONS = {
select_all: "Vælg alle",
deselect_all: "Fravælg alle",
remove_selected: "Fjern valgte",
costs: "*Engangsomkostning for indlejringer",
save_embed: "Gem og indlejr",
"total-documents_one": "{{count}} dokument",
"total-documents_other": "{{count}} dokumenter",

View File

@@ -2,7 +2,6 @@
const TRANSLATIONS = {
onboarding: {
home: {
title: "Willkommen bei",
getStarted: "Jetzt starten",
welcome: "Herzlich willkommen",
},
@@ -315,11 +314,6 @@ const TRANSLATIONS = {
description:
"Aktivieren Sie den Standard-Agenten, um verschiedene Arten von Diagrammen aus bereitgestellten oder im Chat gegebenen Daten zu generieren.",
},
save: {
title: "Dateien generieren & im Browser speichern",
description:
"Aktivieren Sie den Standard-Agenten, um Dateien zu generieren und zu schreiben, die gespeichert und in Ihrem Browser heruntergeladen werden können.",
},
web: {
title: "Live-Websuche und -Browsing",
description:
@@ -365,11 +359,6 @@ const TRANSLATIONS = {
title: "Dateieninformationen abrufen",
description: "Erhalten Sie detaillierte Metadaten über Dateien.",
},
"write-file": {
title: "Datei erstellen",
description:
"Neue Dateien erstellen oder vorhandene Dateien überschreiben.",
},
"edit-file": {
title: "Datei bearbeiten",
description:
@@ -388,6 +377,44 @@ const TRANSLATIONS = {
title: "Datei kopieren",
description: "Dateien und Verzeichnisse kopieren",
},
"write-text-file": {
title: "Textdatei erstellen",
description:
"Erstellen Sie neue Textdateien oder überschreiben Sie vorhandene Textdateien.",
},
},
},
createFiles: {
title: "Dokumentenerstellung",
description:
"Ermöglichen Sie Ihrem Agenten, binäre Dokumentformate wie PowerPoint-Präsentationen, Excel-Tabellen, Word-Dokumente und PDFs zu erstellen. Die Dateien können direkt aus dem Chat-Fenster heruntergeladen werden.",
configuration: "Verfügbare Dokumenttypen",
skills: {
"create-text-file": {
title: "Textdateien",
description:
"Erstellen Sie Textdateien mit beliebigen Inhalten und Dateiendungen (.txt, .md, .json, .csv usw.)",
},
"create-pptx": {
title: "Präsentationen mit PowerPoint",
description:
"Erstellen Sie neue PowerPoint-Präsentationen mit Folien, Überschriften und Stichpunkten.",
},
"create-pdf": {
title: "PDF-Dokumente",
description:
"Erstellen Sie PDF-Dokumente aus Markdown- oder reinen Textdateien mit grundlegender Formatierung.",
},
"create-xlsx": {
title: "Excel-Tabellen",
description:
"Erstellen Sie Excel-Dokumente für tabellarische Daten mit Tabellen und Formatierungen.",
},
"create-docx": {
title: "Word-Dokumente",
description:
"Erstellen Sie Word-Dokumente mit grundlegender Formatierung und Gestaltung.",
},
},
},
},
@@ -813,7 +840,6 @@ const TRANSLATIONS = {
deselect_all: "Auswahl abbrechen",
no_docs: "Keine Dokumente vorhanden.",
remove_selected: "Ausgewähltes entfernen",
costs: "*Einmalige Kosten für das Einbetten",
save_embed: "Speichern und Einbetten",
"total-documents_one": "{{count}} Dokument",
"total-documents_other": "{{count}} Dokumente",

View File

@@ -1,7 +1,6 @@
const TRANSLATIONS = {
onboarding: {
home: {
title: "Welcome to",
welcome: "Welcome",
getStarted: "Get Started",
},
@@ -323,11 +322,6 @@ const TRANSLATIONS = {
description:
"Enable the default agent to generate various types of charts from data provided or given in chat.",
},
save: {
title: "Generate & save files",
description:
"Enable the default agent to generate and write to files that can be saved to your computer.",
},
web: {
title: "Web Search",
description:
@@ -370,9 +364,10 @@ const TRANSLATIONS = {
title: "Get File Info",
description: "Get detailed metadata about files",
},
"write-file": {
title: "Write File",
description: "Create new files or overwrite existing files",
"write-text-file": {
title: "Write Text File",
description:
"Create new text files or overwrite existing text files",
},
"edit-file": {
title: "Edit File",
@@ -392,6 +387,39 @@ const TRANSLATIONS = {
},
},
},
createFiles: {
title: "Document Creation",
description:
"Enable your agent to create binary document formats like PowerPoint presentations, Excel spreadsheets, Word documents, and PDFs. Files can be downloaded directly from the chat window.",
configuration: "Available Document Types",
skills: {
"create-text-file": {
title: "Text Files",
description:
"Create text files with any content and extension (.txt, .md, .json, .csv, etc.)",
},
"create-pptx": {
title: "PowerPoint Presentations",
description:
"Create new PowerPoint presentations with slides, titles, and bullet points",
},
"create-pdf": {
title: "PDF Documents",
description:
"Create PDF documents from markdown or plain text with basic styling",
},
"create-xlsx": {
title: "Excel Spreadsheets",
description:
"Create Excel documents for tabular data with sheets and styling",
},
"create-docx": {
title: "Word Documents",
description:
"Create Word documents with basic styling and formatting",
},
},
},
default_skill:
"By default, this skill is enabled, but you can disable it if you don't want it to be available to the agent.",
},
@@ -909,7 +937,6 @@ const TRANSLATIONS = {
select_all: "Select All",
deselect_all: "Deselect All",
remove_selected: "Remove Selected",
costs: "*One time cost for embeddings",
save_embed: "Save and Embed",
},
upload: {

View File

@@ -2,7 +2,6 @@
const TRANSLATIONS = {
onboarding: {
home: {
title: "Bienvenido a",
getStarted: "Comenzar",
welcome: "Bienvenido",
},
@@ -321,11 +320,6 @@ const TRANSLATIONS = {
description:
"Habilita al agente predeterminado para generar varios tipos de gráficos a partir de datos proporcionados o dados en el chat.",
},
save: {
title: "Generar y guardar archivos en el navegador",
description:
"Habilita al agente predeterminado para generar y escribir en archivos que se guardan y se pueden descargar en tu navegador.",
},
web: {
title: "Búsqueda y navegación web en vivo",
description:
@@ -372,11 +366,6 @@ const TRANSLATIONS = {
description:
"Obtenga información detallada sobre los metadatos de los archivos.",
},
"write-file": {
title: "Crear archivo",
description:
"Crear nuevos archivos o sobrescribir archivos existentes.",
},
"edit-file": {
title: "Editar archivo",
description:
@@ -394,6 +383,44 @@ const TRANSLATIONS = {
title: "Copiar archivo",
description: "Copiar archivos y directorios",
},
"write-text-file": {
title: "Crear un archivo de texto",
description:
"Cree nuevos archivos de texto o sobrescriba archivos de texto existentes.",
},
},
},
createFiles: {
title: "Creación de documentos",
description:
"Permita que su agente cree formatos de documentos binarios como presentaciones de PowerPoint, hojas de cálculo de Excel, documentos de Word y archivos PDF. Los archivos se pueden descargar directamente desde la ventana de chat.",
configuration: "Tipos de documentos disponibles",
skills: {
"create-text-file": {
title: "Archivos de texto",
description:
"Cree archivos de texto con cualquier contenido y extensión (por ejemplo, .txt, .md, .json, .csv, etc.).",
},
"create-pptx": {
title: "Presentaciones en PowerPoint",
description:
"Crea nuevas presentaciones de PowerPoint con diapositivas, títulos y viñetas.",
},
"create-pdf": {
title: "Documentos en formato PDF",
description:
"Cree documentos PDF a partir de archivos Markdown o texto plano, con un estilo básico.",
},
"create-xlsx": {
title: "Hojas de cálculo de Excel",
description:
"Cree documentos de Excel para datos tabulares, con hojas y opciones de formato.",
},
"create-docx": {
title: "Documentos de Microsoft Word",
description:
"Crea documentos de Word con un estilo y formato básicos.",
},
},
},
},
@@ -822,7 +849,6 @@ const TRANSLATIONS = {
select_all: "Seleccionar todo",
deselect_all: "Deseleccionar todo",
remove_selected: "Eliminar seleccionados",
costs: "*Costo único por incrustaciones",
save_embed: "Guardar e incrustar",
"total-documents_one": "{{count}} documento",
"total-documents_other": "{{count}} documentos",

View File

@@ -2,7 +2,6 @@
const TRANSLATIONS = {
onboarding: {
home: {
title: "Tere tulemast",
getStarted: "Alusta",
welcome: "Tere tulemast",
},
@@ -307,11 +306,6 @@ const TRANSLATIONS = {
title: "Loo diagramme",
description: "Lubab agendil luua erinevaid diagramme antud andmetest.",
},
save: {
title: "Loo ja salvesta faile brauserisse",
description:
"Lubab agendil luua faile, mis salvestatakse ja allalaaditakse brauseris.",
},
web: {
title: "Reaalajas veebihaku tugi",
description:
@@ -356,10 +350,6 @@ const TRANSLATIONS = {
title: "Hankige faili teave",
description: "Hankige üksikasjalik teavet failide kohta",
},
"write-file": {
title: "Faili loomine",
description: "Loo uusi faili või asenda olemasoleva faili",
},
"edit-file": {
title: "Faili redigeerimine",
description: "Muuda teksti failide sisu rida- järgselt.",
@@ -376,6 +366,44 @@ const TRANSLATIONS = {
title: "Kopeeri fail",
description: "Kopeeri failid ja kaardi",
},
"write-text-file": {
title: "Loo teksti fail",
description:
"Loo uusi teksti faili või asenda olemasolevaid teksti faile",
},
},
},
createFiles: {
title: "Dokumendi koostamine",
description:
"Laske oma esindajal luua binaarsed dokumendiformaadid, näiteks PowerPointi esitlused, Exceli lehed, Wordi dokumendid ja PDF-failid. Failid saab alla laadida otse vestlusaknast.",
configuration: "Saadaval olevad dokumendi tüübid",
skills: {
"create-text-file": {
title: "Tekstifailid",
description:
"Loo teksti failid, milles on mis tahes sisu ja laiendus (esimärkid .txt, .md, .json, .csv jne).",
},
"create-pptx": {
title: "PowerPointi esitlused",
description:
"Loo uusi PowerPointi esitlusi, mis sisaldavad slaidide, pealkirjade ja punktide.",
},
"create-pdf": {
title: "PDF-failid",
description:
"Loo PDF-failid Markdown- või tavaline tekstist, kasutades lihtsaid stiilid",
},
"create-xlsx": {
title: "Excel-i lehed",
description:
"Loo Excel-failid, mis sisaldavad tabeli andmeid, lehtede ja stiilidega.",
},
"create-docx": {
title: "Microsoft Word-failid",
description:
"Loo Microsoft Wordi dokumende, kasutades põhise stiili ja vormistuse",
},
},
},
},
@@ -778,7 +806,6 @@ const TRANSLATIONS = {
select_all: "Vali kõik",
deselect_all: "Tühista valik",
remove_selected: "Eemalda valitud",
costs: "*Ühekordne embeddingu kulu",
save_embed: "Salvesta ja põimi",
"total-documents_one": "{{count}} dokument",
"total-documents_other": "{{count}} dokumendid",

View File

@@ -17,7 +17,6 @@ const TRANSLATIONS = {
"ما را در ساخت مدل AnythingLLM متناسب با نیازهای شما یاری دهید. (این بخش اختیاری است)",
},
home: {
title: "به",
getStarted: "شروع کنید",
welcome: "به شما خوش آمد می‌گوییم",
},
@@ -310,11 +309,6 @@ const TRANSLATIONS = {
description:
"به عامل پیش‌فرض امکان تولید انواع مختلف نمودار از داده‌های ارائه شده یا داده شده در گفتگو را بدهید.",
},
save: {
title: "تولید و ذخیره فایل‌ها در مرورگر",
description:
"به عامل پیش‌فرض امکان تولید و نوشتن در فایل‌هایی که ذخیره می‌شوند و می‌توانند در مرورگر شما دانلود شوند را بدهید.",
},
web: {
title: "جستجو و مرور زنده وب",
description:
@@ -358,10 +352,6 @@ const TRANSLATIONS = {
title: "اطلاعات فایل را دریافت کنید",
description: "اطلاعات دقیق در مورد فایل‌ها را دریافت کنید.",
},
"write-file": {
title: "ایجاد فایل",
description: "ایجاد فایل‌های جدید یا جایگزینی فایل‌های موجود",
},
"edit-file": {
title: "ویرایش فایل",
description: "امکان ویرایش متون از طریق ویرایش خط به خط",
@@ -378,6 +368,44 @@ const TRANSLATIONS = {
title: "کپی فایل",
description: "فایل‌ها و دایرکتوری‌ها را کپی کنید.",
},
"write-text-file": {
title: "ایجاد یک فایل متنی",
description:
"ایجاد فایل‌های متنی جدید یا جایگزینی فایل‌های متنی موجود",
},
},
},
createFiles: {
title: "ایجاد مستند",
description:
"به کارگزار خود اجازه دهید فرمت‌های اسناد دودویی مانند ارائه پاورپوینت، صفحات گسترده اکسل، اسناد ورد و فایل‌های PDF را ایجاد کند. کاربران می‌توانند فایل‌ها را مستقیماً از پنجره چت دانلود کنند.",
configuration: "انواع اسناد موجود",
skills: {
"create-text-file": {
title: "فایل‌های متنی",
description:
"ایجاد فایل‌های متنی با هر نوع محتوا و پسوند (مانند .txt، .md، .json، .csv و غیره)",
},
"create-pptx": {
title: "ارائه های پاورپوینت",
description:
"ایجاد اسلایدها، عناوین و فهرست‌های بولت برای ارائه پاورپوینت‌های جدید",
},
"create-pdf": {
title: "اسناد به فرمت PDF",
description:
"ایجاد اسناد PDF از متن ساده یا فرمت‌های Markdown با استفاده از استایل‌های پایه",
},
"create-xlsx": {
title: "برگه های اکسل",
description:
"ایجاد فایل‌های اکسل حاوی داده‌های جدولی با جداول و استایل‌های مختلف",
},
"create-docx": {
title: "اسناد و فایل‌های وورد",
description:
"ایجاد فایل‌های Word با استفاده از قالب‌بندی و استایل‌های ساده",
},
},
},
},
@@ -696,7 +724,6 @@ const TRANSLATIONS = {
select_all: "انتخاب همه",
deselect_all: "انتخاب همه را لغو کنید",
remove_selected: "حذف انتخاب‌شده",
costs: "*هزینه یکباره برای ایجاد مدل‌های برداری",
save_embed: "ذخیره و وارد کردن",
"total-documents_one": "{{count}} سند",
"total-documents_other": "{{count}} اسناد",

View File

@@ -16,7 +16,6 @@ const TRANSLATIONS = {
"Aidez-nous à améliorer AnythingLLM en répondant à quelques questions.",
},
home: {
title: "Bienvenue",
getStarted: "Commencer",
welcome: "Bienvenue",
},
@@ -311,11 +310,6 @@ const TRANSLATIONS = {
description:
"Activez l'agent par défaut pour générer différents types de graphiques à partir des données fournies ou données dans le chat.",
},
save: {
title: "Générer et sauvegarder des fichiers dans le navigateur",
description:
"Activez l'agent par défaut pour générer et écrire des fichiers qui peuvent être sauvegardés et téléchargés dans votre navigateur.",
},
web: {
title: "Recherche web en direct et navigation",
description:
@@ -361,11 +355,6 @@ const TRANSLATIONS = {
title: "Obtenir des informations sur le fichier",
description: "Obtenez des métadonnées détaillées sur les fichiers.",
},
"write-file": {
title: "Créer un fichier",
description:
"Créer de nouveaux fichiers ou remplacer des fichiers existants.",
},
"edit-file": {
title: "Modifier le fichier",
description:
@@ -384,6 +373,44 @@ const TRANSLATIONS = {
title: "Copier le fichier",
description: "Copier des fichiers et des répertoires",
},
"write-text-file": {
title: "Créer un fichier texte",
description:
"Créer de nouveaux fichiers texte ou remplacer des fichiers texte existants.",
},
},
},
createFiles: {
title: "Création de documents",
description:
"Permettez à votre agent de créer des documents au format binaire, tels que des présentations PowerPoint, des feuilles de calcul Excel, des documents Word et des fichiers PDF. Les fichiers peuvent être téléchargés directement depuis la fenêtre de chat.",
configuration: "Types de documents disponibles",
skills: {
"create-text-file": {
title: "Fichiers de texte",
description:
"Créez des fichiers texte avec n'importe quel contenu et extension (par exemple, .txt, .md, .json, .csv, etc.)",
},
"create-pptx": {
title: "Présentations PowerPoint",
description:
"Créez de nouvelles présentations PowerPoint avec des diapositives, des titres et des puces.",
},
"create-pdf": {
title: "Documents au format PDF",
description:
"Créez des documents PDF à partir de fichiers Markdown ou de texte brut, avec un style de base.",
},
"create-xlsx": {
title: "Feuilles de calcul Excel",
description:
"Créez des documents Excel pour les données tabulaires, avec des feuilles et un style.",
},
"create-docx": {
title: "Documents au format Word",
description:
"Créez des documents Word avec un style et une mise en page de base.",
},
},
},
},
@@ -703,7 +730,6 @@ const TRANSLATIONS = {
select_all: "Tout sélectionner",
deselect_all: "Tout désélectionner",
remove_selected: "Supprimer la sélection",
costs: "Coûts",
save_embed: "Sauvegarder et intégrer",
"total-documents_one": "{{count}}",
"total-documents_other": "{{count}} documents",

View File

@@ -2,7 +2,6 @@
const TRANSLATIONS = {
onboarding: {
home: {
title: "ברוכים הבאים ל",
getStarted: "להתחלה",
welcome: "ברוכים הבאים",
},
@@ -309,11 +308,6 @@ const TRANSLATIONS = {
description:
"אפשר לסוכן ברירת המחדל ליצור סוגים שונים של תרשימים מנתונים שסופקו או ניתנו בצ'אט.",
},
save: {
title: "יצירה ושמירה של קבצים לדפדפן",
description:
"אפשר לסוכן ברירת המחדל ליצור ולכתוב לקבצים שנשמרים וניתנים להורדה בדפדפן שלך.",
},
web: {
title: "חיפוש וגלישה באינטרנט בזמן אמת",
description:
@@ -357,10 +351,6 @@ const TRANSLATIONS = {
title: "קבל מידע על הקובץ",
description: "קבל מידע מפורט על קבצים",
},
"write-file": {
title: "יצירת קובץ",
description: "יצירת קבצים חדשים או החלפת קבצים קיימים",
},
"edit-file": {
title: "ערוך קובץ",
description: "בצעו עריכה של קבצי טקסט על בסיס שורות.",
@@ -377,6 +367,42 @@ const TRANSLATIONS = {
title: "העתק קובץ",
description: "העתקת קבצים וספריות",
},
"write-text-file": {
title: "יצירת קובץ טקסט",
description: "יצירת קבצי טקסט חדשים או החלפת קבצי טקסט קיימים",
},
},
},
createFiles: {
title: "יצירת מסמכים",
description:
"אפשרו למתווך שלכם ליצור פורמטים של מסמכים בינאריים, כמו מצגות של פאוורPoint, גיליונות אלקטרוניים של Excel, מסמכי Word ו-PDF. ניתן להוריד את הקבצים ישירות מהחלון של השיחה.",
configuration: "סוגי המסמכים הזמינים",
skills: {
"create-text-file": {
title: "קבצי טקסט",
description:
"צרו קבצי טקסט עם כל תוכן ותוספת (כגון .txt, .md, .json, .csv וכו').",
},
"create-pptx": {
title: "מצגות של פאוור פוינט",
description:
"צרו מצגות חדשות בפורמט PowerPoint, הכוללות שקופיות, כותרות ונקודות עיקריות.",
},
"create-pdf": {
title: "מסמכים בפורמט PDF",
description:
"יצירת מסמכים בפורמט PDF ממחרוזות markdown או טקסט פשוט, עם עיצוב בסיסי.",
},
"create-xlsx": {
title: "גיליונות אקסל",
description:
"צור מסמכי Excel עבור נתונים טבלאיים, הכוללים גיליונות ועריכת עיצוב.",
},
"create-docx": {
title: "מסמכים בפורמט Word",
description: "צור מסמכים בפורמט Word עם עיצוב ועיצוב בסיסיים.",
},
},
},
},
@@ -781,7 +807,6 @@ const TRANSLATIONS = {
select_all: "בחר הכל",
deselect_all: "בטל בחירת הכל",
remove_selected: "הסר נבחרים",
costs: "*עלות חד פעמית להטמעות",
save_embed: "שמור והטמע",
"total-documents_one": "{{count}} מסמך",
"total-documents_other": "מסמכים {{count}}",

View File

@@ -17,7 +17,6 @@ const TRANSLATIONS = {
"Aiutaci a sviluppare AnythingLLM in base alle tue esigenze. Facoltativo.",
},
home: {
title: "Benvenuti a",
getStarted: "Inizia",
welcome: "Benvenuti",
},
@@ -314,11 +313,6 @@ const TRANSLATIONS = {
description:
"Consenti all'agente predefinito di generare vari tipi di grafici dai dati forniti o forniti nella chat.",
},
save: {
title: "Genera e salva file nel browser",
description:
"Abilita l'agente predefinito per generare e scrivere su file che possono essere salvati e scaricati nel tuo browser.",
},
web: {
title: "Ricerca e navigazione web in tempo reale",
description:
@@ -364,10 +358,6 @@ const TRANSLATIONS = {
title: "Ottieni informazioni sul file",
description: "Ottenere metadati dettagliati sui file.",
},
"write-file": {
title: "Creare file",
description: "Creare nuovi file o sovrascrivere i file esistenti.",
},
"edit-file": {
title: "Modifica file",
description: "Applica modifiche basate su righe ai file di testo.",
@@ -384,6 +374,44 @@ const TRANSLATIONS = {
title: "Copia file",
description: "Copia file e directory",
},
"write-text-file": {
title: "Crea un file di testo",
description:
"Creare nuovi file di testo o sovrascrivere file di testo esistenti.",
},
},
},
createFiles: {
title: "Creazione di documenti",
description:
"Permetti al tuo agente di creare file in formati binari come presentazioni PowerPoint, fogli di calcolo Excel, documenti Word e file PDF. I file possono essere scaricati direttamente dalla finestra di chat.",
configuration: "Tipi di documenti disponibili",
skills: {
"create-text-file": {
title: "File di testo",
description:
"Creare file di testo con qualsiasi contenuto ed estensione (ad esempio, .txt, .md, .json, .csv, ecc.)",
},
"create-pptx": {
title: "Presentazioni PowerPoint",
description:
"Crea nuove presentazioni PowerPoint con diapositive, titoli e punti elenco.",
},
"create-pdf": {
title: "Documenti in formato PDF",
description:
"Creare documenti PDF da file Markdown o testo semplice, con un'applicazione di formattazione di base.",
},
"create-xlsx": {
title: "Fogli di calcolo Excel",
description:
"Crea documenti Excel per dati tabulari, con fogli e stili.",
},
"create-docx": {
title: "Documenti in formato Word",
description:
"Creare documenti Word con un'impaginazione e formattazione di base.",
},
},
},
},
@@ -705,7 +733,6 @@ const TRANSLATIONS = {
select_all: "Seleziona tutto",
deselect_all: "Deselect All",
remove_selected: "Elimina gli elementi selezionati",
costs: "*Costo una tantum per le embedding",
save_embed: "Salva e incorpora",
"total-documents_one": "{{count}} documento",
"total-documents_other": "{{count}} documenti",

View File

@@ -2,7 +2,6 @@
const TRANSLATIONS = {
onboarding: {
home: {
title: "ようこそ",
getStarted: "はじめる",
welcome: "ようこそ",
},
@@ -306,11 +305,6 @@ const TRANSLATIONS = {
description:
"デフォルトエージェントがチャットやデータからさまざまなチャートを作成できるようにします。",
},
save: {
title: "ファイルの生成と保存",
description:
"デフォルトエージェントがファイルを生成し、ブラウザからダウンロードできるようにします。",
},
web: {
title: "ウェブ検索と閲覧",
description:
@@ -355,11 +349,6 @@ const TRANSLATIONS = {
title: "ファイルの情報を取得する",
description: "ファイルに関する詳細なメタデータを取得する",
},
"write-file": {
title: "ファイルを作成",
description:
"新しいファイルを作成するか、既存のファイルを上書きする",
},
"edit-file": {
title: "ファイル編集",
description: "テキストファイルの行単位での編集を行う",
@@ -376,6 +365,43 @@ const TRANSLATIONS = {
title: "ファイルのコピー",
description: "ファイルとディレクトリをコピーする",
},
"write-text-file": {
title: "テキストファイルを作成する",
description:
"新しいテキストファイルを作成するか、既存のテキストファイルを上書きする。",
},
},
},
createFiles: {
title: "ドキュメント作成",
description:
"エージェントが、パワーポイント、Excel、Word、PDFなどのバイナリ形式のドキュメントを作成できるようにします。ファイルはチャットウィンドウから直接ダウンロードできます。",
configuration: "利用可能なドキュメントの種類",
skills: {
"create-text-file": {
title: "テキストファイル",
description:
".txt、.md、.json、.csvなどの拡張子を持つ、任意のコンテンツのテキストファイルを作成する。",
},
"create-pptx": {
title: "パワーポイント形式のプレゼンテーション",
description:
"スライド、タイトル、箇条書きを含む、新しいPowerPointプレゼンテーションを作成する。",
},
"create-pdf": {
title: "PDFドキュメント",
description:
"マークダウンまたはプレーンテキストから、基本的な書式設定を使用してPDFドキュメントを作成する。",
},
"create-xlsx": {
title: "エクセル スプレッドシート",
description:
"表形式のデータをスプレッドシート形式で作成し、シートとスタイルを設定する。",
},
"create-docx": {
title: "Wordドキュメント",
description: "基本的なスタイルと書式でWordドキュメントを作成する",
},
},
},
},
@@ -690,7 +716,6 @@ const TRANSLATIONS = {
select_all: "すべて選択",
deselect_all: "すべて選択解除",
remove_selected: "選択したものを削除",
costs: "※埋め込みには一度だけ費用がかかります",
save_embed: "保存して埋め込む",
"total-documents_one": "{{count}} のドキュメント",
"total-documents_other": "{{count}} に関する書類",

View File

@@ -2,7 +2,6 @@
const TRANSLATIONS = {
onboarding: {
home: {
title: "방문을 환영합니다",
getStarted: "시작하기",
welcome: "환영합니다",
},
@@ -310,11 +309,6 @@ const TRANSLATIONS = {
description:
"기본 에이전트가 채팅에서 제공된 데이터를 이용하여 다양한 유형의 차트를 생성할 수 있도록 합니다.",
},
save: {
title: "브라우저에서 파일 생성과 저장",
description:
"기본 에이전트가 브라우저에서 파일을 생성하고 다운로드할 수 있도록 합니다.",
},
web: {
title: "실시간 웹 검색 및 탐색",
description:
@@ -359,10 +353,6 @@ const TRANSLATIONS = {
title: "파일 정보 확인",
description: "파일에 대한 자세한 메타데이터를 얻으세요.",
},
"write-file": {
title: "파일 작성",
description: "새로운 파일을 생성하거나 기존 파일을 덮어쓰기",
},
"edit-file": {
title: "파일 편집",
description: "텍스트 파일에 줄 단위로 편집",
@@ -379,6 +369,44 @@ const TRANSLATIONS = {
title: "파일 복사",
description: "파일 및 디렉터리를 복사",
},
"write-text-file": {
title: "텍스트 파일 작성",
description:
"새로운 텍스트 파일을 생성하거나 기존 텍스트 파일을 덮어쓰기",
},
},
},
createFiles: {
title: "문서 생성",
description:
"에이전트가 파워포인트 프레젠테이션, 엑셀 스프레드시트, 워드 문서, PDF 등 이진 파일 형식의 문서를 생성할 수 있도록 지원합니다. 파일은 채팅 창에서 직접 다운로드할 수 있습니다.",
configuration: "사용 가능한 문서 유형",
skills: {
"create-text-file": {
title: "텍스트 파일",
description:
"원하는 내용과 확장자(.txt, .md, .json, .csv 등)를 사용하여 텍스트 파일을 생성합니다.",
},
"create-pptx": {
title: "파워포인트 프레젠테이션",
description:
"슬라이드, 제목, 마크다운 등을 포함하여 새로운 파워포인트 프레젠테이션을 만드세요.",
},
"create-pdf": {
title: "PDF 문서",
description:
"기본 스타일을 적용하여 마크다운 또는 일반 텍스트에서 PDF 문서를 생성합니다.",
},
"create-xlsx": {
title: "엑셀 스프레드시트",
description:
"표와 스타일을 포함한 표 형태의 데이터를 위한 엑셀 문서를 만드세요.",
},
"create-docx": {
title: "워드 문서",
description:
"기본적인 스타일과 서식을 사용하여 워드 문서를 만드세요.",
},
},
},
},
@@ -790,7 +818,6 @@ const TRANSLATIONS = {
select_all: "전체 선택",
deselect_all: "전체 선택 해제",
remove_selected: "선택 항목 삭제",
costs: "*임베딩 1회 비용",
save_embed: "저장 및 임베딩",
"total-documents_one": "{{count}} 문서",
"total-documents_other": "{{count}} 관련 문서",

View File

@@ -2,7 +2,6 @@
const TRANSLATIONS = {
onboarding: {
home: {
title: "Sveiki atvykę į",
getStarted: "Pradėti",
welcome: "Sveiki",
},
@@ -323,11 +322,6 @@ const TRANSLATIONS = {
description:
"Leidžia numatytajam agentui generuoti įvairių tipų diagramas pagal pateiktus arba pokalbio metu gautus duomenis.",
},
save: {
title: "Generuoti ir išsaugoti failus",
description:
"Leidžia numatytajam agentui generuoti ir rašyti failus, kuriuos galima išsaugoti jūsų kompiuteryje.",
},
web: {
title: "Paieška internete",
description:
@@ -372,10 +366,6 @@ const TRANSLATIONS = {
title: "Gaukite failo informaciją",
description: "Gaukite išsamią informaciją apie failus.",
},
"write-file": {
title: "Sukurti failą",
description: "Sukurti naujus failus arba pakeisti esamus",
},
"edit-file": {
title: "Redaguoti failą",
description: "Atlikite teksto failų redakciją, remdamiesi eilėmis.",
@@ -392,6 +382,44 @@ const TRANSLATIONS = {
title: "Kopijuoti failą",
description: "Kopijuoti failus ir katalogus",
},
"write-text-file": {
title: "Sukurti teksto failą",
description:
"Sukurkite naujus tekstinius failus arba pakeiskite esamus tekstinius failus.",
},
},
},
createFiles: {
title: "Dokumento sukūrimas",
description:
"Įgalinkite savo agentą kurti dvigubos formos dokumentų formatus, tokius kaip „PowerPoint“ prezentacijos, „Excel“ lentelės, „Word“ dokumentai ir PDF failus. Failus galima atsisiųsti tiesiai iš pokalbio lango.",
configuration: "Galimi dokumentų tipai",
skills: {
"create-text-file": {
title: "Tekstiniai failai",
description:
"Sukurkite teksto failus su bet kokiu turiniu ir failų sąlypa (.txt, .md, .json, .csv ir kt.)",
},
"create-pptx": {
title: "„PowerPoint“ pristatymai",
description:
"Sukurkite naujas PowerPoint prezentacijas, naudodami slaidus, pavadinimus ir punktų sąrašus.",
},
"create-pdf": {
title: "PDF dokumentai",
description:
"Sukurkite PDF dokumentus iš Markdown ar paprastos teksto formato, naudodami pagrindinius stiliaus elementus.",
},
"create-xlsx": {
title: "„Excel“ lentelės",
description:
"Sukurkite Excel dokumentus, skirtus lentelių duomenims, su lapais ir stiliaus parametrais.",
},
"create-docx": {
title: "Skelbimo dokumentai",
description:
"Sukurkite Word dokumentus su pagrindine stiliavimo ir formavimo funkcija.",
},
},
},
},
@@ -834,7 +862,6 @@ const TRANSLATIONS = {
select_all: "Žymėti viską",
deselect_all: "Atžymėti viską",
remove_selected: "Šalinti pažymėtus",
costs: "*Vienkartinis mokestis už vektorių kūrimą",
save_embed: "Išsaugoti ir kurti vektorius",
},
upload: {

View File

@@ -2,7 +2,6 @@
const TRANSLATIONS = {
onboarding: {
home: {
title: "Laipni lūgti",
getStarted: "Sākt darbu",
welcome: "Laipni lūdzam",
},
@@ -315,11 +314,6 @@ const TRANSLATIONS = {
description:
"Iespējot noklusējuma aģentam ģenerēt dažāda veida diagrammas no sarunā sniegtajiem vai dotajiem datiem.",
},
save: {
title: "Ģenerēt un saglabāt failus pārlūkā",
description:
"Iespējot noklusējuma aģentam ģenerēt un rakstīt failus, kas saglabājas un var tikt lejupielādēti jūsu pārlūkā.",
},
web: {
title: "Tiešsaistes tīmekļa meklēšana un pārlūkošana",
description:
@@ -365,10 +359,6 @@ const TRANSLATIONS = {
title: "Iegūst faila informāciju",
description: "Iesaļojiet detalizētus failu metadatus",
},
"write-file": {
title: "Izveidot failu",
description: "Izveidot jaunas failus vai pārrakstīt esošus failus",
},
"edit-file": {
title: "Rediģēt failu",
description:
@@ -386,6 +376,44 @@ const TRANSLATIONS = {
title: "Kopēt failu",
description: "Kopēt failus un direktorus",
},
"write-text-file": {
title: "Izveidot teksta failu",
description:
"Izveidot jaunas teksta failus vai pārrakstīt esošos teksta failus.",
},
},
},
createFiles: {
title: "Dokumentu izveide",
description:
"Ļauj savam pārstāvim izveidot binārus dokumentu formātus, piemēram, PowerPoint prezentācijas, Excel tabulas, Word dokumentus un PDF failus. Failus var lejupielādēt tieši no čata.",
configuration: "Pieejamās dokumentu veidas",
skills: {
"create-text-file": {
title: "Teksta faili",
description:
"Izveidot teksta failus ar jebkuru saturu un izplejumu (.txt, .md, .json, .csv utt.)",
},
"create-pptx": {
title: "PowerPoint prezentācijas",
description:
"Izveidot jaunas PowerPoint prezentācijas ar slaidiem, nosaukumiem un punktiem.",
},
"create-pdf": {
title: "PDF dokumenti",
description:
"Izveidot PDF dokumentus no Markdown vai vienkāršas teksta, izmantojot pamata formāciju.",
},
"create-xlsx": {
title: "Excel tabulas",
description:
"Izveidot Excel dokumentus ar tabulas datiem, kas ietver lapas un stila iespējas.",
},
"create-docx": {
title: "Vārdu dokumenti",
description:
"Izveidot Word dokumentus ar pamata stils un formātēšanu",
},
},
},
},
@@ -805,7 +833,6 @@ const TRANSLATIONS = {
select_all: "Atlasīt visu",
deselect_all: "Atcelt visu atlasi",
remove_selected: "Noņemt atlasītos",
costs: "*Vienreizējas izmaksas iegulšanai",
save_embed: "Saglabāt un iegult",
"total-documents_one": "{{count}} dokumenta",
"total-documents_other": "{{count}} dokumenti",

View File

@@ -17,7 +17,6 @@ const TRANSLATIONS = {
"Help ons AnythingLLM af te stemmen op jouw behoeften. (Optioneel)",
},
home: {
title: "Welkom bij",
getStarted: "Aan de slag",
welcome: "Welkom",
},
@@ -310,11 +309,6 @@ const TRANSLATIONS = {
description:
"Sta de standaardagent toe om verschillende soorten grafieken te genereren uit verstrekte of in de chat gegeven gegevens.",
},
save: {
title: "Genereren & opslaan van bestanden naar browser",
description:
"Sta de standaardagent toe om te genereren en te schrijven naar bestanden die worden opgeslagen en kunnen worden gedownload in je browser.",
},
web: {
title: "Live web zoeken en browsen",
description:
@@ -361,11 +355,6 @@ const TRANSLATIONS = {
title: "Fijlsinformatie bekijken",
description: "Verkrijg gedetailleerde metadata over bestanden.",
},
"write-file": {
title: "Schrijf bestand",
description:
"Maak nieuwe bestanden aan of vervang bestaande bestanden.",
},
"edit-file": {
title: "Bestand bewerken",
description: "Voer wijzigingen uit op tekstbestanden, per regel.",
@@ -382,6 +371,43 @@ const TRANSLATIONS = {
title: "Kopieer bestand",
description: "Kopieer bestanden en mappen",
},
"write-text-file": {
title: "Maak een tekstbestand",
description:
"Maak nieuwe tekstbestanden aan of vervang bestaande tekstbestanden.",
},
},
},
createFiles: {
title: "Documentcreatie",
description:
"Laat uw agent documenten in binair formaat aanmaken, zoals PowerPoint-presentaties, Excel-tabellen, Word-documenten en PDF-bestanden. Bestanden kunnen direct worden gedownload vanuit het chatvenster.",
configuration: "Beschikbare documenttypen",
skills: {
"create-text-file": {
title: "Tekstbestanden",
description:
"Maak tekstbestanden met elk gewenst inhoud en bestandsextensie (bijvoorbeeld .txt, .md, .json, .csv, enz.).",
},
"create-pptx": {
title: "Presentaties met PowerPoint",
description:
"Maak nieuwe PowerPoint-presentaties met dia's, titels en opsommingstekens.",
},
"create-pdf": {
title: "PDF-documenten",
description:
"Maak PDF-documenten aan vanuit Markdown of gewone tekst, met basisstijling.",
},
"create-xlsx": {
title: "Excel-tabellen",
description:
"Maak Excel-documenten voor tabelgegevens, met tabbladen en opmaak.",
},
"create-docx": {
title: "Word-documenten",
description: "Maak Word-documenten met basisstijlen en -opmaak.",
},
},
},
},
@@ -702,7 +728,6 @@ const TRANSLATIONS = {
select_all: "Alles selecteren",
deselect_all: "Alles deselecteren",
remove_selected: "Verwijderen Geselecteerd",
costs: "*Eenmalige kosten voor embedden",
save_embed: "Opslaan en embedden",
"total-documents_one": "{{count}} document",
"total-documents_other": "{{count}} documenten",

View File

@@ -2,7 +2,6 @@
const TRANSLATIONS = {
onboarding: {
home: {
title: "Witamy w",
getStarted: "Rozpocznij",
welcome: "Witaj",
},
@@ -316,11 +315,6 @@ const TRANSLATIONS = {
description:
"Pozwól domyślnemu agentowi generować różne typy wykresów na podstawie danych dostarczonych lub podanych na czacie.",
},
save: {
title: "Generowanie i zapisywanie plików w przeglądarce",
description:
"Pozwól domyślnemu agentowi generować i zapisywać pliki, które można zapisać i pobrać w przeglądarce.",
},
web: {
title: "Wyszukiwanie i przeglądanie stron internetowych na żywo",
description:
@@ -366,10 +360,6 @@ const TRANSLATIONS = {
title: "Pobierz informacje o pliku",
description: "Uzyskaj szczegółowe metadane dotyczące plików.",
},
"write-file": {
title: "Utwórz plik",
description: "Utwórz nowe pliki lub nadpisz istniejące",
},
"edit-file": {
title: "Edytuj plik",
description:
@@ -387,6 +377,44 @@ const TRANSLATIONS = {
title: "Skopiuj plik",
description: "Kopiuj pliki i katalogi",
},
"write-text-file": {
title: "Utwórz plik tekstowy",
description:
"Utwórz nowe pliki tekstowe lub nadpisz istniejące pliki tekstowe.",
},
},
},
createFiles: {
title: "Tworzenie dokumentów",
description:
"Pozwól swojemu agentowi tworzyć pliki w formatach binarnych, takich jak prezentacje PowerPoint, arkusze kalkulacyjne Excel, dokumenty Word i pliki PDF. Pliki można pobrać bezpośrednio z okna czatu.",
configuration: "Dostępne typy dokumentów",
skills: {
"create-text-file": {
title: "Pliki tekstowe",
description:
"Utwórz pliki tekstowe z dowolnym zawartością i rozszerzeniem (np. .txt, .md, .json, .csv).",
},
"create-pptx": {
title: "Prezentacje w formacie PowerPoint",
description:
"Stwórz nowe prezentacje w formacie PowerPoint, zawierające slajdy, nagłówki i punkty.",
},
"create-pdf": {
title: "Dokumenty w formacie PDF",
description:
"Tworzenie dokumentów PDF z plików w formacie Markdown lub zwykłego tekstu, z podstawowymi możliwościami stylizacji.",
},
"create-xlsx": {
title: "Arkusze kalkulacyjne w programie Excel",
description:
"Stwórz arkusze kalkulacyjne w programie Excel, zawierające dane w formie tabel, z różnymi arkuszami i stylami.",
},
"create-docx": {
title: "Dokumenty w formacie Word",
description:
"Stwórz dokumenty Word z podstawowymi stylami i formatowaniem.",
},
},
},
},
@@ -806,7 +834,6 @@ const TRANSLATIONS = {
select_all: "Wybierz wszystko",
deselect_all: "Odznacz wszystko",
remove_selected: "Usuń wybrane",
costs: "*Jednorazowy koszt dodania danych",
save_embed: "Zapisz",
"total-documents_one": "{{count}} dokument",
"total-documents_other": "{{count}} dokumenty",

View File

@@ -2,7 +2,6 @@
const TRANSLATIONS = {
onboarding: {
home: {
title: "Bem-vindo ao",
getStarted: "Começar",
welcome: "Bem-vindo",
},
@@ -315,10 +314,6 @@ const TRANSLATIONS = {
description:
"Permite ao agente padrão gerar diversos tipos de gráficos a partir de dados armazenados ou informados no chat.",
},
save: {
title: "Gerar & salvar arquivos",
description: "Permite ao agente gerar e salvar arquivos no navegador.",
},
web: {
title: "Busca na web",
description:
@@ -363,11 +358,6 @@ const TRANSLATIONS = {
title: "Obter informações do arquivo",
description: "Obtenha metadados detalhados sobre os arquivos.",
},
"write-file": {
title: "Criar arquivo",
description:
"Criar novos arquivos ou substituir arquivos existentes.",
},
"edit-file": {
title: "Editar arquivo",
description:
@@ -385,6 +375,44 @@ const TRANSLATIONS = {
title: "Copiar arquivo",
description: "Copie arquivos e diretórios",
},
"write-text-file": {
title: "Criar um arquivo de texto",
description:
"Crie novos arquivos de texto ou sobrescreva arquivos de texto existentes.",
},
},
},
createFiles: {
title: "Criação de documentos",
description:
"Permita que seu agente crie formatos de documentos binários, como apresentações do PowerPoint, planilhas do Excel, documentos do Word e arquivos PDF. Os arquivos podem ser baixados diretamente da janela de chat.",
configuration: "Tipos de documentos disponíveis",
skills: {
"create-text-file": {
title: "Arquivos de texto",
description:
"Crie arquivos de texto com qualquer conteúdo e extensão (por exemplo, .txt, .md, .json, .csv, etc.)",
},
"create-pptx": {
title: "Apresentações em PowerPoint",
description:
"Crie novas apresentações do PowerPoint com slides, títulos e marcadores.",
},
"create-pdf": {
title: "Documentos em formato PDF",
description:
"Crie documentos em PDF a partir de arquivos Markdown ou texto simples, com formatação básica.",
},
"create-xlsx": {
title: "Planilhas do Excel",
description:
"Crie documentos do Excel para dados tabulares, incluindo planilhas e estilos.",
},
"create-docx": {
title: "Documentos do tipo Word",
description:
"Crie documentos do Word com formatação e estilo básicos.",
},
},
},
},
@@ -791,7 +819,6 @@ const TRANSLATIONS = {
select_all: "Selecionar Tudo",
deselect_all: "Desmarcar Tudo",
remove_selected: "Remover Selecionados",
costs: "*Custo único para vínculos",
save_embed: "Salvar e Inserir",
"total-documents_one": "{{count}} documento",
"total-documents_other": "{{count}} documentos",

View File

@@ -2,7 +2,6 @@
const TRANSLATIONS = {
onboarding: {
home: {
title: "Bine ai venit la",
getStarted: "Începe",
welcome: "Bine ați venit",
},
@@ -457,7 +456,6 @@ const TRANSLATIONS = {
select_all: "Selectează tot",
deselect_all: "Deselectează tot",
remove_selected: "Elimină selectate",
costs: "*Cost unic pentru embeddings",
save_embed: "Salvează și încorporează",
"total-documents_one": "{{count}}",
"total-documents_other": "{{count}} documente",
@@ -767,11 +765,6 @@ const TRANSLATIONS = {
description:
"Permite agentului implicit să genereze diverse tipuri de grafice din datele furnizate sau date în chat.",
},
save: {
title: "Generează & salvează fișiere în browser",
description:
"Permite agentului implicit să genereze și să scrie fișiere care se salvează și pot fi descărcate în browserul tău.",
},
web: {
title: "Căutare și navigare web live",
description:
@@ -817,11 +810,6 @@ const TRANSLATIONS = {
title: "Obține informații despre fișier",
description: "Obțineți metadate detaliate despre fișiere.",
},
"write-file": {
title: "Creați fișier",
description:
"Creați fișiere noi sau suprascrieți fișierele existente.",
},
"edit-file": {
title: "Modifică fișierul",
description: "Realizați modificări pe linii în fișierele de text.",
@@ -838,6 +826,44 @@ const TRANSLATIONS = {
title: "Copiază fișier",
description: "Copiați fișiere și directoare",
},
"write-text-file": {
title: "Creați un fișier de text",
description:
"Creați fișiere de text noi sau suprascrieți fișierele de text existente.",
},
},
},
createFiles: {
title: "Crearea de documente",
description:
"Permite-ți agentului să creeze formate de documente binare, cum ar fi prezentări PowerPoint, fișe Excel, documente Word și fișiere PDF. Fișierele pot fi descărcate direct din fereastra de chat.",
configuration: "Tipuri de documente disponibile",
skills: {
"create-text-file": {
title: "Fișiere text",
description:
"Creați fișiere text cu orice conținut și extensie (de exemplu, .txt, .md, .json, .csv, etc.)",
},
"create-pptx": {
title: "Prezentări PowerPoint",
description:
"Creați prezentări noi în PowerPoint, cu diapozitive, titluri și puncte.",
},
"create-pdf": {
title: "Documente în format PDF",
description:
"Creați documente PDF din fișiere Markdown sau text simplu, cu un stil de formatare de bază.",
},
"create-xlsx": {
title: "Fișe Excel",
description:
"Creați fișiere Excel pentru date tabulare, cu foi și stilizare.",
},
"create-docx": {
title: "Fișiere în format Word",
description:
"Creați documente Word cu un stil și formatare de bază.",
},
},
},
},

View File

@@ -2,7 +2,6 @@
const TRANSLATIONS = {
onboarding: {
home: {
title: "Добро пожаловать в",
getStarted: "Начать",
welcome: "Добро пожаловать",
},
@@ -309,11 +308,6 @@ const TRANSLATIONS = {
description:
"Включите возможность создания различных типов диаграмм из предоставленных данных или данных, указанных в чате.",
},
save: {
title: "Создание и сохранение файлов в браузер",
description:
"Включите возможность создания и записи файлов, которые можно сохранить и загрузить в вашем браузере.",
},
web: {
title: "Поиск в Интернете и просмотр в реальном времени",
description:
@@ -358,10 +352,6 @@ const TRANSLATIONS = {
title: "Получить информацию о файле",
description: "Получите подробную информацию о метаданных файлов.",
},
"write-file": {
title: "Создать файл",
description: "Создать новые файлы или перезаписать существующие",
},
"edit-file": {
title: "Редактировать файл",
description:
@@ -379,6 +369,44 @@ const TRANSLATIONS = {
title: "Скопировать файл",
description: "Копировать файлы и каталоги",
},
"write-text-file": {
title: "Создать текстовый файл",
description:
"Создайте новые текстовые файлы или перезапишите существующие текстовые файлы.",
},
},
},
createFiles: {
title: "Создание документа",
description:
"Предоставьте своему агенту возможность создавать файлы в двоичном формате, такие как презентации PowerPoint, электронные таблицы Excel, документы Word и PDF. Файлы можно загружать непосредственно из окна чата.",
configuration: "Доступные типы документов",
skills: {
"create-text-file": {
title: "Текстовые файлы",
description:
"Создавайте текстовые файлы с любым содержимым и расширением (.txt, .md, .json, .csv и т. д.)",
},
"create-pptx": {
title: "Презентации в формате PowerPoint",
description:
"Создайте новые презентации в формате PowerPoint, включая слайды, заголовки и списки с маркерами.",
},
"create-pdf": {
title: "Документы в формате PDF",
description:
"Создавайте документы в формате PDF из Markdown или простого текста с использованием базового оформления.",
},
"create-xlsx": {
title: "Электронные таблицы в программе Excel",
description:
"Создайте документы Excel для табличных данных, включающие листы и оформление.",
},
"create-docx": {
title: "Документы в формате Word",
description:
"Создавайте документы в формате Word с использованием базовых стилей и форматирования.",
},
},
},
},
@@ -698,7 +726,6 @@ const TRANSLATIONS = {
select_all: "Выбрать всё",
deselect_all: "Снять выбор со всех",
remove_selected: "Удалить выбранные",
costs: "*Единоразовая стоимость за внедрение",
save_embed: "Сохранить и внедрить",
"total-documents_one": "{{count}} документ",
"total-documents_other": "{{count}} документы",

View File

@@ -17,7 +17,6 @@ const TRANSLATIONS = {
"AnythingLLM'yi ihtiyaçlarınıza göre oluşturmamıza yardımcı olun. İsteğe bağlı.",
},
home: {
title: "Hoş Geldiniz",
getStarted: "Başla",
welcome: "Hoş geldiniz",
},
@@ -309,11 +308,6 @@ const TRANSLATIONS = {
description:
"Varsayılan ajanın, sağlanan veya sohbette yer alan verilere göre çeşitli grafik türleri oluşturmasına izin verin.",
},
save: {
title: "Tarayıcıya dosya oluştur & kaydet",
description:
"Varsayılan ajanın, oluşturduğu dosyaları kaydetmesine ve tarayıcıda indirilebilir hale getirmesine izin verin.",
},
web: {
title: "Canlı web araması ve gezinme",
description:
@@ -359,11 +353,6 @@ const TRANSLATIONS = {
title: "Dosya Hakkında Bilgi Al",
description: "Dosyalara ilişkin ayrıntılı meta verileri elde edin.",
},
"write-file": {
title: "Dosya Oluştur",
description:
"Yeni dosyalar oluşturun veya mevcut dosyaları üzerine yazın.",
},
"edit-file": {
title: "Dosya Düzenle",
description: "Metin dosyalarında satır bazlı değişiklikler yapın.",
@@ -381,6 +370,44 @@ const TRANSLATIONS = {
title: "Dosyayı Kopyala",
description: "Dosyaları ve dizinleri kopyala",
},
"write-text-file": {
title: "Metin dosyası oluştur",
description:
"Yeni metin dosyaları oluşturun veya mevcut metin dosyalarını üzerine yazın.",
},
},
},
createFiles: {
title: "Belge Oluşturma",
description:
"Temsilcinizin, PowerPoint sunumları, Excel tabloları, Word belgeleri ve PDF dosyaları gibi ikili belge formatları oluşturmasına olanak tanıyın. Dosyalar doğrudan sohbet penceresinden indirilebilir.",
configuration: "Mevcut Belgeler Türleri",
skills: {
"create-text-file": {
title: "Metin dosyaları",
description:
"Herhangi bir içerik ve uzantıyla (örneğin .txt, .md, .json, .csv vb.) metin dosyaları oluşturun.",
},
"create-pptx": {
title: "PowerPoint sunumları",
description:
"Yeni PowerPoint sunumları oluşturun, slaytlar, başlıklar ve madde işaretleri ekleyin.",
},
"create-pdf": {
title: "PDF belgeleri",
description:
"Temel stillerle markdown veya düz metinlerden PDF belgeleri oluşturun.",
},
"create-xlsx": {
title: "Excel elektronik tabloları",
description:
"Tablo formatındaki veriler için, sayfalar ve stil özellikleriyle Excel belgeleri oluşturun.",
},
"create-docx": {
title: "Kelime belgeleri",
description:
"Temel stil ve biçimlendirme ile Word belgeleri oluşturun.",
},
},
},
},
@@ -700,7 +727,6 @@ const TRANSLATIONS = {
select_all: "Tümünü Seç",
deselect_all: "Tümünün Seçimini Kaldır",
remove_selected: "Seçilenleri Kaldır",
costs: "*Gömmeler için tek seferlik maliyet",
save_embed: "Kaydet ve Göm",
"total-documents_one": "{{count}} belgesi",
"total-documents_other": "{{count}} belgeleri",

View File

@@ -17,7 +17,6 @@ const TRANSLATIONS = {
"Giúp chúng tôi xây dựng AnythingLLM phù hợp với nhu cầu của bạn. Tùy chọn.",
},
home: {
title: "Chào mừng đến",
getStarted: "Bắt đầu",
welcome: "Chào mừng",
},
@@ -308,11 +307,6 @@ const TRANSLATIONS = {
description:
"Cho phép agent mặc định tạo các loại biểu đồ khác nhau từ dữ liệu được cung cấp hoặc đưa ra trong trò chuyện.",
},
save: {
title: "Tạo & lưu tệp",
description:
"Cho phép agent mặc định tạo và ghi vào các tệp có thể lưu vào máy tính của bạn.",
},
web: {
title: "Tìm kiếm web trực tiếp và duyệt web",
description:
@@ -357,10 +351,6 @@ const TRANSLATIONS = {
title: "Lấy thông tin tệp",
description: "Lấy thông tin chi tiết về các tệp tin.",
},
"write-file": {
title: "Tạo tệp",
description: "Tạo các tệp mới hoặc ghi đè các tệp hiện có",
},
"edit-file": {
title: "Chỉnh sửa tệp",
description:
@@ -378,6 +368,44 @@ const TRANSLATIONS = {
title: "Sao chép tệp",
description: "Sao chép các tệp tin và thư mục",
},
"write-text-file": {
title: "Tạo tệp văn bản",
description:
"Tạo các tệp văn bản mới hoặc ghi đè các tệp văn bản hiện có.",
},
},
},
createFiles: {
title: "Tạo tài liệu",
description:
"Cho phép đại lý của bạn tạo các định dạng tài liệu nhị phân như bài thuyết trình PowerPoint, bảng tính Excel, tài liệu Word và PDF. Các tệp có thể tải xuống trực tiếp từ cửa sổ trò chuyện.",
configuration: "Các loại tài liệu có sẵn",
skills: {
"create-text-file": {
title: "Tệp văn bản",
description:
"Tạo các tệp văn bản với bất kỳ nội dung và định dạng nào (ví dụ: .txt, .md, .json, .csv, v.v.)",
},
"create-pptx": {
title: "Bài trình bày bằng PowerPoint",
description:
"Tạo các bài trình chiếu PowerPoint mới với các slide, tiêu đề và dấu đầu dòng.",
},
"create-pdf": {
title: "Tài liệu PDF",
description:
"Tạo tài liệu PDF từ Markdown hoặc văn bản thuần túy với các định dạng cơ bản.",
},
"create-xlsx": {
title: "Bảng tính Excel",
description:
"Tạo các tài liệu Excel cho dữ liệu dạng bảng, bao gồm các sheet và định dạng.",
},
"create-docx": {
title: "Tệp Word",
description:
"Tạo các tài liệu Word với định dạng và kiểu dáng cơ bản.",
},
},
},
},
@@ -695,7 +723,6 @@ const TRANSLATIONS = {
select_all: "Chọn Tất cả",
deselect_all: "Bỏ chọn Tất cả",
remove_selected: "Xóa Đã chọn",
costs: "*Chi phí một lần cho việc nhúng",
save_embed: "Lưu và Nhúng",
"total-documents_one": "{{count}}",
"total-documents_other": "{{count}}",

View File

@@ -2,7 +2,6 @@
const TRANSLATIONS = {
onboarding: {
home: {
title: "欢迎使用",
getStarted: "开始",
welcome: "欢迎",
},
@@ -300,11 +299,6 @@ const TRANSLATIONS = {
title: "生成图表",
description: "使默认代理能够从提供的数据或聊天中生成各种类型的图表。",
},
save: {
title: "生成并保存文件到浏览器",
description:
"使默认代理能够生成并写入文件,这些文件可以保存并在你的浏览器中下载。",
},
web: {
title: "实时网络搜索和浏览",
description:
@@ -348,10 +342,6 @@ const TRANSLATIONS = {
title: "获取文件信息",
description: "获取有关文件的详细元数据",
},
"write-file": {
title: "创建文件",
description: "创建新的文件或覆盖现有文件",
},
"edit-file": {
title: "编辑文件",
description: "对文本文件进行基于行的编辑。",
@@ -368,6 +358,40 @@ const TRANSLATIONS = {
title: "复制文件",
description: "复制文件和目录",
},
"write-text-file": {
title: "创建文本文件",
description: "创建新的文本文件,或覆盖现有的文本文件。",
},
},
},
createFiles: {
title: "文档创建",
description:
"允许您的代理创建二进制文档格式例如PowerPoint演示文稿、Excel电子表格、Word文档和PDF文件。文件可以直接从聊天窗口下载。",
configuration: "可用的文件类型",
skills: {
"create-text-file": {
title: "文本文件",
description:
"创建包含任何内容和扩展名的文本文件(如 .txt、.md、.json、.csv 等)。",
},
"create-pptx": {
title: "PowerPoint 演示文稿",
description: "创建新的幻灯片演示文稿,包括幻灯片、标题和项目符号。",
},
"create-pdf": {
title: "PDF 文档",
description:
"使用 Markdown 或纯文本,并进行基本的排版,创建 PDF 文档。",
},
"create-xlsx": {
title: "Excel电子表格",
description: "创建包含表格数据、工作表和样式的 Excel 文档。",
},
"create-docx": {
title: "Word 文档",
description: "创建包含基本样式和格式的 Word 文档",
},
},
},
},
@@ -744,7 +768,6 @@ const TRANSLATIONS = {
select_all: "全选",
deselect_all: "取消全选",
remove_selected: "移除所选",
costs: "*嵌入时一次性费用",
save_embed: "保存并嵌入",
"total-documents_one": "{{count}} 文件",
"total-documents_other": "{{count}} 类型的文件",

View File

@@ -2,7 +2,6 @@
const TRANSLATIONS = {
onboarding: {
home: {
title: "歡迎使用",
getStarted: "開始使用",
welcome: "歡迎",
},
@@ -291,10 +290,6 @@ const TRANSLATIONS = {
description:
"讓預設智慧代理人能夠根據提供的資料或對話中給定的資料來產生各種圖表。",
},
save: {
title: "產生並儲存檔案",
description: "讓預設智慧代理人產生並寫入檔案,之後可儲存到電腦。",
},
web: {
title: "網頁搜尋",
description:
@@ -337,10 +332,6 @@ const TRANSLATIONS = {
title: "取得檔案資訊",
description: "獲取關於檔案的詳細元數據",
},
"write-file": {
title: "儲存檔案",
description: "建立新的檔案或覆蓋現有檔案",
},
"edit-file": {
title: "編輯檔案",
description: "能夠對文字檔案進行行別編輯。",
@@ -357,6 +348,40 @@ const TRANSLATIONS = {
title: "複製檔案",
description: "複製檔案和目錄",
},
"write-text-file": {
title: "撰寫文字檔案",
description: "建立新的文字檔,或覆蓋現有的文字檔。",
},
},
},
createFiles: {
title: "文件建立",
description:
"允許您的代理創建二元文件格式例如PowerPoint簡報、Excel電子表格、Word文件和PDF文件。 文件可以直接從聊天窗口下載。",
configuration: "可用的文件類型",
skills: {
"create-text-file": {
title: "文字檔",
description:
"能夠創建包含任何內容和檔案擴展名(例如:.txt、.md、.json、.csv 等)的文字檔案。",
},
"create-pptx": {
title: "PowerPoint 簡報",
description: "創建新的 PowerPoint 簡報,包含幻燈片、標題和要點",
},
"create-pdf": {
title: "PDF 文件",
description:
"能夠從 Markdown 或純文字檔案中,使用基本的格式設定,創建 PDF 文件。",
},
"create-xlsx": {
title: "Excel 試算表",
description: "建立包含表格資料、工作表和樣式的 Excel 文件",
},
"create-docx": {
title: "Word 格式的文件",
description: "建立包含基本樣式和格式的 Word 文件",
},
},
},
},
@@ -654,7 +679,6 @@ const TRANSLATIONS = {
select_all: "全選",
deselect_all: "取消全選",
remove_selected: "移除選擇的項目",
costs: "*嵌入僅會計費一次",
save_embed: "儲存並嵌入",
"total-documents_one": "{{count}} 文件",
"total-documents_other": "{{count}} 文件",

View File

@@ -0,0 +1,26 @@
import { API_BASE } from "@/utils/constants";
import { baseHeaders } from "@/utils/request";
const StorageFiles = {
/**
* Download a file from the server
* @param {string} filename - The filename to download
* @returns {Promise<Blob|null>}
*/
download: async function (storageFilename) {
return await fetch(
`${API_BASE}/agent-skills/generated-files/${encodeURIComponent(storageFilename)}`,
{ headers: baseHeaders() }
)
.then((res) => {
if (!res.ok) throw new Error("Failed to download file");
return res.blob();
})
.catch((e) => {
console.error("Download failed:", e);
return null;
});
},
};
export default StorageFiles;

View File

@@ -848,6 +848,21 @@ const System = {
.catch(() => false);
},
/**
* Checks if the create-files-agent skill is available.
* The create-files-agent skill is only available when running in a Docker container.
* @returns {Promise<boolean>}
*/
isCreateFilesAgentAvailable: async function () {
return fetch(`${API_BASE}/agent-skills/create-files-agent/is-available`, {
method: "GET",
headers: baseHeaders(),
})
.then((res) => res.json())
.then((res) => res?.available ?? false)
.catch(() => false);
},
experimentalFeatures: {
liveSync: LiveDocumentSync,
agentPlugins: AgentPlugins,

View File

@@ -0,0 +1,187 @@
import React, { useEffect, useState, useRef } from "react";
import Toggle, { SimpleToggleSwitch } from "@/components/lib/Toggle";
import { useTranslation } from "react-i18next";
import {
FilePpt,
FileXls,
FileDoc,
FilePdf,
FileText,
CircleNotch,
} from "@phosphor-icons/react";
import Admin from "@/models/admin";
const getCreateFileSkills = (t) => [
{
name: "create-text-file",
title: t("agent.skill.createFiles.skills.create-text-file.title"),
description: t(
"agent.skill.createFiles.skills.create-text-file.description"
),
icon: FileText,
},
{
name: "create-pptx-presentation",
title: t("agent.skill.createFiles.skills.create-pptx.title"),
description: t("agent.skill.createFiles.skills.create-pptx.description"),
icon: FilePpt,
},
{
name: "create-pdf-file",
title: t("agent.skill.createFiles.skills.create-pdf.title"),
description: t("agent.skill.createFiles.skills.create-pdf.description"),
icon: FilePdf,
},
{
name: "create-excel-file",
title: t("agent.skill.createFiles.skills.create-xlsx.title"),
description: t("agent.skill.createFiles.skills.create-xlsx.description"),
icon: FileXls,
},
{
name: "create-docx-file",
title: t("agent.skill.createFiles.skills.create-docx.title"),
description: t("agent.skill.createFiles.skills.create-docx.description"),
icon: FileDoc,
},
];
export default function CreateFileSkillPanel({
title,
skill,
toggleSkill,
enabled = false,
disabled = false,
image,
icon,
setHasChanges,
hasChanges = false,
}) {
const { t } = useTranslation();
const [disabledSkills, setDisabledSkills] = useState([]);
const [loading, setLoading] = useState(true);
const prevHasChanges = useRef(hasChanges);
const skills = getCreateFileSkills(t);
useEffect(() => {
setLoading(true);
Admin.systemPreferencesByFields(["disabled_create_files_skills"])
.then((res) =>
setDisabledSkills(res?.settings?.disabled_create_files_skills ?? [])
)
.catch(() => setDisabledSkills([]))
.finally(() => setLoading(false));
}, []);
useEffect(() => {
if (prevHasChanges.current === true && hasChanges === false) {
Admin.systemPreferencesByFields(["disabled_create_files_skills"])
.then((res) =>
setDisabledSkills(res?.settings?.disabled_create_files_skills ?? [])
)
.catch(() => {});
}
prevHasChanges.current = hasChanges;
}, [hasChanges]);
function toggleFileSkill(skillName) {
setHasChanges(true);
setDisabledSkills((prev) =>
prev.includes(skillName)
? prev.filter((s) => s !== skillName)
: [...prev, skillName]
);
}
return (
<div className="p-2">
<div className="flex flex-col gap-y-[18px] max-w-[500px]">
<div className="flex w-full justify-between items-center">
<div className="flex items-center gap-x-2">
{icon &&
React.createElement(icon, {
size: 24,
color: "var(--theme-text-primary)",
weight: "bold",
})}
<label
htmlFor="name"
className="text-theme-text-primary text-md font-bold"
>
{title}
</label>
</div>
<Toggle
size="lg"
enabled={enabled}
disabled={disabled}
onChange={() => toggleSkill(skill)}
/>
</div>
<img src={image} alt={title} className="w-full rounded-md" />
<p className="text-theme-text-secondary text-opacity-60 text-xs font-medium">
{t("agent.skill.createFiles.description")}
</p>
{enabled && (
<>
<input
name="system::disabled_create_files_skills"
type="hidden"
value={disabledSkills.join(",")}
/>
<div className="flex flex-col mt-2 gap-y-2">
<p className="text-theme-text-primary font-semibold text-sm">
{t("agent.skill.createFiles.configuration")}
</p>
{loading ? (
<div className="flex items-center justify-center py-4">
<CircleNotch
size={24}
className="animate-spin text-theme-text-primary"
/>
</div>
) : (
<div className="flex flex-col gap-y-2">
{skills.map((fileSkill) => (
<SkillRow
key={fileSkill.name}
skill={fileSkill}
disabled={disabledSkills.includes(fileSkill.name)}
onToggle={() => toggleFileSkill(fileSkill.name)}
/>
))}
</div>
)}
</div>
</>
)}
</div>
</div>
);
}
function SkillRow({ skill, disabled, onToggle }) {
const Icon = skill.icon;
return (
<div
className={`flex items-center justify-between p-2 rounded-lg border ${
disabled
? "bg-theme-bg-secondary/30 border-theme-sidebar-border/30"
: "bg-theme-bg-secondary/50 border-theme-sidebar-border/50"
}`}
>
<div className="flex items-center gap-x-2">
<Icon size={22} className="text-slate-100 shrink-0" />
<div className="flex flex-col">
<span className="text-sm font-medium text-slate-100">
{skill.title}
</span>
<span className="text-xs text-slate-100/50">{skill.description}</span>
</div>
</div>
<SimpleToggleSwitch enabled={!disabled} onChange={onToggle} size="md" />
</div>
);
}

View File

@@ -62,9 +62,11 @@ const getFileSystemSubSkills = (t) => {
category: "read",
},
{
name: "filesystem-write-file",
title: t("agent.skill.filesystem.skills.write-file.title"),
description: t("agent.skill.filesystem.skills.write-file.description"),
name: "filesystem-write-text-file",
title: t("agent.skill.filesystem.skills.write-text-file.title"),
description: t(
"agent.skill.filesystem.skills.write-text-file.description"
),
icon: FloppyDisk,
category: "write",
},

View File

@@ -60,10 +60,13 @@ export default function AdminAgents() {
const [fileSystemAgentAvailable, setFileSystemAgentAvailable] =
useState(false);
const [createFilesAgentAvailable, setCreateFilesAgentAvailable] =
useState(false);
const defaultSkills = getDefaultSkills(t);
const configurableSkills = getConfigurableSkills(t, {
fileSystemAgentAvailable,
createFilesAgentAvailable,
});
// Alert user if they try to leave the page with unsaved changes
@@ -82,8 +85,13 @@ export default function AdminAgents() {
useEffect(() => {
async function fetchSettings() {
const [_settings, _preferences, flowsRes, fsAgentAvailable] =
await Promise.all([
const [
_settings,
_preferences,
flowsRes,
fsAgentAvailable,
createFilesAvailable,
] = await Promise.all([
System.keys(),
Admin.systemPreferencesByFields([
"disabled_agent_skills",
@@ -93,6 +101,7 @@ export default function AdminAgents() {
]),
AgentFlows.listFlows(),
System.isFileSystemAgentAvailable(),
System.isCreateFilesAgentAvailable(),
]);
const { flows = [] } = flowsRes;
@@ -105,6 +114,7 @@ export default function AdminAgents() {
setActiveFlowIds(_preferences.settings?.active_agent_flows ?? []);
setAgentFlows(flows);
setFileSystemAgentAvailable(fsAgentAvailable);
setCreateFilesAgentAvailable(createFilesAvailable);
setLoading(false);
}
fetchSettings();

View File

@@ -3,13 +3,14 @@ import AgentSQLConnectorSelection from "./SQLConnectorSelection";
import GenericSkillPanel from "./GenericSkillPanel";
import DefaultSkillPanel from "./DefaultSkillPanel";
import FileSystemSkillPanel from "./FileSystemSkillPanel";
import CreateFileSkillPanel from "./CreateFileSkillPanel";
import {
Brain,
File,
Browser,
ChartBar,
FileMagnifyingGlass,
FolderOpen,
FilePlus,
} from "@phosphor-icons/react";
import RAGImage from "@/media/agents/rag-memory.png";
import SummarizeImage from "@/media/agents/view-summarize.png";
@@ -47,7 +48,7 @@ export const getDefaultSkills = (t) => ({
export const getConfigurableSkills = (
t,
{ fileSystemAgentAvailable = true } = {}
{ fileSystemAgentAvailable = true, createFilesAgentAvailable = true } = {}
) => ({
...(fileSystemAgentAvailable && {
"filesystem-agent": {
@@ -59,14 +60,16 @@ export const getConfigurableSkills = (
image: FileSystemImage,
},
}),
"save-file-to-browser": {
title: t("agent.skill.save.title"),
description: t("agent.skill.save.description"),
component: GenericSkillPanel,
skill: "save-file-to-browser",
icon: FileMagnifyingGlass,
...(createFilesAgentAvailable && {
"create-files-agent": {
title: t("agent.skill.createFiles.title"),
description: t("agent.skill.createFiles.description"),
component: CreateFileSkillPanel,
skill: "create-files-agent",
icon: FilePlus,
image: GenerateSaveImages,
},
}),
"create-chart": {
title: t("agent.skill.generate.title"),
description: t("agent.skill.generate.description"),

View File

@@ -1,6 +1,5 @@
import { v4 } from "uuid";
import { safeJsonParse } from "../request";
import { saveAs } from "file-saver";
import { API_BASE } from "../constants";
import { useEffect, useState } from "react";
@@ -8,7 +7,7 @@ export const AGENT_SESSION_START = "agentSessionStart";
export const AGENT_SESSION_END = "agentSessionEnd";
const handledEvents = [
"statusResponse",
"fileDownload",
"fileDownloadCard",
"awaitingFeedback",
"wssFailure",
"rechartVisualize",
@@ -91,6 +90,9 @@ export default function handleSocketResponse(socket, event, setChatHistory) {
// Providers like Gemini send large chunks and can complete in a single chunk before the update logic can convert it.
// Other providers send many small chunks so the second chunk triggers the update logic to fix the type.
if (data.content.type === "textResponseChunk") {
// If this first chunk is just a non-text char (like \n, \t, etc.) then we need to ignore it.
// Some providers like LMStudio will do this and it depends on the chat template as well.
if (data.content.content.trim() === "") return prev;
return [
...prev.filter((msg) => !!msg.content),
{
@@ -157,6 +159,7 @@ export default function handleSocketResponse(socket, event, setChatHistory) {
}
if (type === "textResponseChunk") {
console.log("textResponseChunk", data.content);
return prev
.map((msg) =>
msg.uuid === uuid
@@ -182,9 +185,24 @@ export default function handleSocketResponse(socket, event, setChatHistory) {
});
}
if (data.type === "fileDownload") {
saveAs(data.content.b64Content, data.content.filename ?? "unknown.txt");
return;
if (data.type === "fileDownloadCard") {
return setChatHistory((prev) => {
return [
...prev.filter((msg) => !!msg.content),
{
type: "fileDownloadCard",
uuid: v4(),
content: data.content,
role: "assistant",
sources: [],
closed: true,
error: null,
animate: false,
pending: false,
metrics: data.metrics || {},
},
];
});
}
if (data.type === "rechartVisualize") {

2
server/.gitignore vendored
View File

@@ -4,6 +4,7 @@
server/.env
storage/assets/*
!storage/assets/anything-llm.png
!storage/assets/anything-llm-invert.png
storage/documents/*
storage/comkey/*
storage/tmp/*
@@ -11,6 +12,7 @@ storage/vector-cache/*.json
storage/exports
storage/imports
storage/anythingllm-fs/*
storage/generated-files/*
storage/plugins/agent-skills/*
storage/plugins/agent-flows/*
storage/plugins/office-extensions/*

View File

@@ -406,6 +406,9 @@ function adminEndpoints(app) {
case "disabled_filesystem_skills":
requestedSettings[label] = safeJsonParse(setting?.value, []);
break;
case "disabled_create_files_skills":
requestedSettings[label] = safeJsonParse(setting?.value, []);
break;
case "imported_agent_skills":
requestedSettings[label] = ImportedPlugin.listImportedPlugins();
break;

View File

@@ -0,0 +1,142 @@
const {
userFromSession,
multiUserMode,
safeJsonParse,
} = require("../utils/http");
const { validatedRequest } = require("../utils/middleware/validatedRequest");
const {
flexUserRoleValid,
ROLES,
} = require("../utils/middleware/multiUserProtected");
const { WorkspaceChats } = require("../models/workspaceChats");
const { Workspace } = require("../models/workspace");
const createFilesLib = require("../utils/agents/aibitat/plugins/create-files/lib");
/**
* Endpoints for serving agent-generated files (PPTX, etc.) with authentication
* and ownership validation.
*/
function agentFileServerEndpoints(app) {
if (!app) return;
/**
* Download a generated file by its storage filename.
* Validates that the requesting user has access to the workspace
* where the file was generated.
*/
app.get(
"/agent-skills/generated-files/:filename",
[validatedRequest, flexUserRoleValid([ROLES.all])],
async (request, response) => {
try {
const user = await userFromSession(request, response);
const { filename } = request.params;
if (!filename)
return response.status(400).json({ error: "Filename is required" });
// Validate filename format
const parsed = createFilesLib.parseFilename(filename);
if (!parsed) {
return response
.status(400)
.json({ error: "Invalid filename format" });
}
// Find a chat record that references this file and that the user can access
const validChat = await findValidChatForFile(
filename,
user,
multiUserMode(response)
);
if (!validChat) {
return response.status(404).json({
error: "File not found or access denied",
});
}
// Retrieve the file from storage
const fileData = await createFilesLib.getGeneratedFile(filename);
if (!fileData) {
return response
.status(404)
.json({ error: "File not found in storage" });
}
// Get mime type and set headers for download
const mimeType = createFilesLib.getMimeType(`.${parsed.extension}`);
const safeFilename = createFilesLib.sanitizeFilenameForHeader(
validChat.displayFilename || filename
);
response.setHeader("Content-Type", mimeType);
response.setHeader(
"Content-Disposition",
`attachment; filename="${safeFilename}"`
);
response.setHeader("Content-Length", fileData.buffer.length);
return response.send(fileData.buffer);
} catch (error) {
console.error("[agentFileServer] Download error:", error.message);
return response.status(500).json({ error: "Failed to download file" });
}
}
);
}
/**
* Finds a valid chat record that references the given storage filename
* and that the user has access to.
* @param {string} storageFilename - The storage filename to search for
* @param {object|null} user - The user object (null in single-user mode)
* @param {boolean} isMultiUser - Whether multi-user mode is enabled
* @returns {Promise<{workspaceId: number, displayFilename: string}|null>}
*/
async function findValidChatForFile(storageFilename, user, isMultiUser) {
try {
// Get all workspaces the user has access to.
// In single-user mode, all workspaces are accessible.
// In multi-user mode, only workspaces assigned to the user are accessible.
let workspaceIds;
if (isMultiUser && user) {
const workspaces = await Workspace.whereWithUser(user);
workspaceIds = workspaces.map((w) => w.id);
} else {
const workspaces = await Workspace.where();
workspaceIds = workspaces.map((w) => w.id);
}
if (workspaceIds.length === 0) return null;
// Use database-level filtering to only fetch chats that contain the filename
// This avoids loading all chats into memory
const chats = await WorkspaceChats.where({
workspaceId: { in: workspaceIds },
include: true,
response: { contains: storageFilename },
});
for (const chat of chats) {
try {
const response = safeJsonParse(chat.response, { outputs: [] });
const output = response.outputs.find(
(o) => o?.payload?.storageFilename === storageFilename
);
if (!output) continue;
return {
workspaceId: chat.workspaceId,
displayFilename:
output.payload.filename || output.payload.displayFilename,
};
} catch {
continue;
}
}
return null;
} catch (error) {
console.error("[findValidChatForFile] Error:", error.message);
return null;
}
}
module.exports = { agentFileServerEndpoints };

View File

@@ -27,6 +27,24 @@ function agentSkillWhitelistEndpoints(app) {
}
);
app.get(
"/agent-skills/create-files-agent/is-available",
[validatedRequest],
async (_request, response) => {
try {
const createFilesTool = require("../utils/agents/aibitat/plugins/create-files/lib");
return response
.status(200)
.json({ available: createFilesTool.isToolAvailable() });
} catch (e) {
console.error(e);
return response
.status(500)
.json({ available: false, error: e.message });
}
}
);
app.post(
"/agent-skills/whitelist/add",
[validatedRequest, flexUserRoleValid(ROLES.all)],

View File

@@ -210,4 +210,5 @@ module.exports = {
getGitVersion,
getModelTag,
getAnythingLLMUserAgent,
getDeploymentVersion,
};

View File

@@ -26,6 +26,7 @@ const { agentWebsocket } = require("./endpoints/agentWebsocket");
const {
agentSkillWhitelistEndpoints,
} = require("./endpoints/agentSkillWhitelist");
const { agentFileServerEndpoints } = require("./endpoints/agentFileServer");
const { experimentalEndpoints } = require("./endpoints/experimental");
const { browserExtensionEndpoints } = require("./endpoints/browserExtension");
const { communityHubEndpoints } = require("./endpoints/communityHub");
@@ -79,6 +80,7 @@ utilEndpoints(apiRouter);
documentEndpoints(apiRouter);
agentWebsocket(apiRouter);
agentSkillWhitelistEndpoints(apiRouter);
agentFileServerEndpoints(apiRouter);
experimentalEndpoints(apiRouter);
developerEndpoints(app, apiRouter);
communityHubEndpoints(apiRouter);

View File

@@ -0,0 +1,108 @@
const { log, conclude } = require("./helpers/index.js");
const { WorkspaceChats } = require("../models/workspaceChats.js");
const createFilesLib = require("../utils/agents/aibitat/plugins/create-files/lib.js");
const { safeJsonParse } = require("../utils/http/index.js");
(async () => {
try {
const fs = require("fs");
const path = require("path");
const storageDirectory = await createFilesLib.getOutputDirectory();
if (!fs.existsSync(storageDirectory)) return;
const files = fs.readdirSync(storageDirectory);
if (files.length === 0) return;
// Get all storage filenames referenced in active (include: true) chats
const activeFileRefs = await getActiveStorageFilenames();
const filesToDelete = [];
for (const filename of files) {
const fullPath = path.join(storageDirectory, filename);
const stat = fs.statSync(fullPath);
// Skip files/folders that don't match our naming pattern and add to deletion list
if (!filename.match(/^[a-z]+-[a-f0-9-]{36}(\.\w+)?$/i)) {
filesToDelete.push({ path: fullPath, isDirectory: stat.isDirectory() });
continue;
}
// If file/folder is not referenced in any active chat, add to deletion list
if (!activeFileRefs.has(filename))
filesToDelete.push({ path: fullPath, isDirectory: stat.isDirectory() });
}
if (filesToDelete.length === 0) return;
log(`Found ${filesToDelete.length} orphaned files/folders to delete.`);
let deletedCount = 0;
let failedCount = 0;
for (const { path: itemPath, isDirectory } of filesToDelete) {
try {
if (isDirectory) fs.rmSync(itemPath, { recursive: true });
else fs.unlinkSync(itemPath);
deletedCount++;
} catch (error) {
failedCount++;
log(`Failed to delete ${itemPath}: ${error.message}`);
}
}
log(
`Cleanup complete: deleted ${deletedCount} items, ${failedCount} failures.`
);
} catch (error) {
console.error(error);
log(`Error during cleanup: ${error.message}`);
} finally {
conclude();
}
})();
/**
* Retrieves all storage filenames referenced in active (include: true) workspace chats.
* Searches through the outputs array in chat responses.
* Uses pagination to avoid loading all chats into memory at once.
* @param {number} batchSize - Number of chats to process per batch (default: 50)
* @returns {Promise<Set<string>>}
*/
async function getActiveStorageFilenames(batchSize = 50) {
const storageFilenames = new Set();
try {
let offset = 0;
let hasMore = true;
while (hasMore) {
const chats = await WorkspaceChats.where(
{ include: true },
batchSize,
{ id: "asc" },
offset
);
if (chats.length === 0) {
hasMore = false;
break;
}
for (const chat of chats) {
try {
const response = safeJsonParse(chat.response, { outputs: [] });
for (const output of response.outputs) {
if (output?.payload?.storageFilename)
storageFilenames.add(output.payload.storageFilename);
}
} catch {
continue;
}
}
offset += chats.length;
hasMore = chats.length === batchSize;
}
} catch (error) {
console.error("[getActiveStorageFilenames] Error:", error.message);
}
return storageFilenames;
}

View File

@@ -34,6 +34,7 @@ const SystemSettings = {
"default_agent_skills",
"disabled_agent_skills",
"disabled_filesystem_skills",
"disabled_create_files_skills",
"imported_agent_skills",
"custom_app_name",
"feature_flags",
@@ -52,6 +53,7 @@ const SystemSettings = {
"default_agent_skills",
"disabled_agent_skills",
"disabled_filesystem_skills",
"disabled_create_files_skills",
"agent_sql_connections",
"custom_app_name",
"default_system_prompt",
@@ -163,6 +165,15 @@ const SystemSettings = {
return JSON.stringify([]);
}
},
disabled_create_files_skills: (updates) => {
try {
const skills = updates.split(",").filter((skill) => !!skill);
return JSON.stringify(skills);
} catch {
console.error(`Could not validate disabled create files skills.`);
return JSON.stringify([]);
}
},
agent_sql_connections: async (updates) => {
const existingConnections = safeJsonParse(
(await SystemSettings.get({ label: "agent_sql_connections" }))?.value,

View File

@@ -32,6 +32,7 @@
"@langchain/core": "0.1.61",
"@langchain/openai": "0.0.28",
"@langchain/textsplitters": "0.0.0",
"@mdpdf/mdpdf": "0.1.4",
"@mintplex-labs/bree": "^9.2.5",
"@mintplex-labs/express-ws": "^5.0.7",
"@modelcontextprotocol/sdk": "^1.24.3",
@@ -54,8 +55,11 @@
"cohere-ai": "^7.19.0",
"cors": "^2.8.5",
"diff": "7.0.0",
"docx": "9.6.1",
"dompurify": "3.3.3",
"dotenv": "^16.0.3",
"elevenlabs": "^0.5.0",
"exceljs": "4.4.0",
"express": "^4.21.2",
"extract-json-from-string": "^1.0.1",
"fast-levenshtein": "^3.0.0",
@@ -67,7 +71,9 @@
"js-tiktoken": "^1.0.8",
"jsonrepair": "^3.7.0",
"jsonwebtoken": "^9.0.0",
"jsdom": "26.1.0",
"langchain": "0.1.36",
"marked": "15.0.12",
"mime": "^3.0.0",
"moment": "^2.29.4",
"mssql": "^10.0.2",
@@ -76,10 +82,12 @@
"node-telegram-bot-api": "^0.67.0",
"ollama": "^0.6.3",
"openai": "4.95.1",
"pdf-lib": "1.17.1",
"pg": "^8.11.5",
"pinecone-client": "^1.1.0",
"pluralize": "^8.0.0",
"posthog-node": "^3.1.1",
"pptxgenjs": "4.0.1",
"prisma": "5.3.1",
"slugify": "^1.6.6",
"strip-ansi": "^7.1.2",

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

View File

@@ -15,6 +15,11 @@ class BackgroundService {
timeout: "1m",
interval: "12hr",
},
{
name: "cleanup-generated-files",
timeout: "5m",
interval: "8hr",
},
];
#documentSyncJobs = [

View File

@@ -53,6 +53,7 @@ const chatHistory = {
const invocation = aibitat.handlerProps.invocation;
const metrics = aibitat.provider?.getUsage?.() ?? {};
const citations = aibitat._pendingCitations ?? [];
const outputs = aibitat._pendingOutputs ?? [];
await WorkspaceChats.new({
workspaceId: Number(invocation.workspace_id),
prompt,
@@ -62,11 +63,13 @@ const chatHistory = {
type: "chat",
attachments,
metrics,
...(outputs.length > 0 ? { outputs } : {}),
},
user: { id: invocation?.user_id || null },
threadId: invocation?.thread_id || null,
});
aibitat.clearCitations?.();
aibitat._pendingOutputs = [];
},
_storeSpecial: async function (
aibitat,
@@ -75,6 +78,7 @@ const chatHistory = {
const invocation = aibitat.handlerProps.invocation;
const metrics = aibitat.provider?.getUsage?.() ?? {};
const citations = aibitat._pendingCitations ?? [];
const outputs = aibitat._pendingOutputs ?? [];
const existingSources = options?.sources ?? [];
await WorkspaceChats.new({
workspaceId: Number(invocation.workspace_id),
@@ -89,11 +93,13 @@ const chatHistory = {
type: options?.saveAsType ?? "chat",
attachments,
metrics,
...(outputs.length > 0 ? { outputs } : {}),
},
user: { id: invocation?.user_id || null },
threadId: invocation?.thread_id || null,
});
aibitat.clearCitations?.();
aibitat._pendingOutputs = [];
options?.postSave();
},
};

View File

@@ -0,0 +1,301 @@
const {
getDeploymentVersion,
} = require("../../../../../../endpoints/utils.js");
const createFilesLib = require("../lib.js");
const {
getTheme,
getMargins,
loadLibraries,
htmlToDocxElements,
createCoverPageSection,
createRunningHeader,
createRunningFooter,
DEFAULT_NUMBERING_CONFIG,
} = require("./utils.js");
module.exports.CreateDocxFile = {
name: "create-docx-file",
plugin: function () {
return {
name: "create-docx-file",
setup(aibitat) {
aibitat.function({
super: aibitat,
name: this.name,
description:
"Create a Microsoft Word document (.docx) from markdown or plain text content. Supports professional styling with color themes, title pages, and running headers/footers.",
examples: [
{
prompt: "Create a Word document with meeting notes",
call: JSON.stringify({
filename: "meeting-notes.docx",
content:
"# Meeting Notes - Q1 Planning\n\n## Attendees\n- John Smith\n- Sarah Johnson\n- Mike Chen\n\n## Agenda\n1. Review Q4 results\n2. Set Q1 goals\n3. Assign tasks\n\n## Action Items\n| Person | Task | Due Date |\n|--------|------|----------|\n| John | Prepare budget report | Jan 15 |\n| Sarah | Draft marketing plan | Jan 20 |\n| Mike | Schedule follow-up | Jan 10 |",
}),
},
{
prompt:
"Create a professional project proposal with a title page",
call: JSON.stringify({
filename: "project-proposal.docx",
title: "Project Alpha Proposal",
subtitle: "Strategic Initiative for Q2 2024",
author: "Product Team",
theme: "blue",
includeTitlePage: true,
content:
"## Executive Summary\nThis proposal outlines the development of **Project Alpha**, a next-generation platform.\n\n## Objectives\n- Increase efficiency by 40%\n- Reduce costs by $50,000 annually\n- Improve user satisfaction\n\n## Timeline\n| Phase | Duration | Deliverables |\n|-------|----------|-------------|\n| Phase 1 | 4 weeks | Requirements |\n| Phase 2 | 8 weeks | Development |\n| Phase 3 | 2 weeks | Testing |\n\n## Budget\nTotal estimated budget: **$150,000**",
}),
},
{
prompt: "Create technical documentation with warm theme",
call: JSON.stringify({
filename: "api-documentation.docx",
title: "API Documentation",
theme: "warm",
margins: "narrow",
content:
"# API Documentation\n\n## Authentication\nAll API requests require a Bearer token in the Authorization header.\n\n```javascript\nconst headers = {\n 'Authorization': 'Bearer YOUR_TOKEN',\n 'Content-Type': 'application/json'\n};\n```\n\n## Endpoints\n\n### GET /users\nReturns a list of all users.\n\n### POST /users\nCreates a new user.\n\n> **Note:** Rate limiting applies to all endpoints.",
}),
},
],
parameters: {
$schema: "http://json-schema.org/draft-07/schema#",
type: "object",
properties: {
filename: {
type: "string",
description:
"The filename for the Word document. Will automatically add .docx extension if not present.",
},
title: {
type: "string",
description:
"Document title for metadata and title page. If not provided, will be extracted from content or use filename.",
},
subtitle: {
type: "string",
description:
"Optional subtitle displayed on the title page below the main title.",
},
author: {
type: "string",
description:
"Optional author name displayed on the title page.",
},
content: {
type: "string",
description:
"The content to convert to a Word document. Fully supports markdown formatting.",
},
theme: {
type: "string",
enum: ["neutral", "blue", "warm"],
description:
"Color theme for the document. 'neutral' (slate/grey), 'blue' (corporate blue), or 'warm' (earthy tones). Defaults to neutral.",
},
margins: {
type: "string",
enum: ["normal", "narrow", "wide"],
description:
"Page margin preset. 'normal' (standard), 'narrow' (data-heavy docs), or 'wide' (letters/memos). Defaults to normal.",
},
includeTitlePage: {
type: "boolean",
description:
"Include a professional title page with centered title, subtitle, author, and date. Content starts on page 2 with running headers/footers.",
},
},
required: ["filename", "content"],
additionalProperties: false,
},
handler: async function ({
filename = "document.docx",
title = null,
subtitle = null,
author = null,
content = "",
theme = "neutral",
margins = "normal",
includeTitlePage = false,
}) {
try {
this.super.handlerProps.log(`Using the create-docx-file tool.`);
const hasExtension = /\.docx$/i.test(filename);
if (!hasExtension) filename = `${filename}.docx`;
const displayFilename = filename.split("/").pop();
const documentTitle =
title ||
content.match(/^#\s+(.+)$/m)?.[1] ||
displayFilename.replace(/\.docx$/i, "");
if (this.super.requestToolApproval) {
const approval = await this.super.requestToolApproval({
skillName: this.name,
payload: { filename: displayFilename, title: documentTitle },
description: `Create Word document "${displayFilename}"`,
});
if (!approval.approved) {
this.super.introspect(
`${this.caller}: User rejected the ${this.name} request.`
);
return approval.message;
}
}
this.super.introspect(
`${this.caller}: Creating Word document "${displayFilename}"${includeTitlePage ? " with title page" : ""}`
);
const libs = await loadLibraries();
const { marked, docx } = libs;
const { Document, Packer, Paragraph, TextRun } = docx;
marked.setOptions({
gfm: true,
breaks: true,
});
const themeColors = getTheme(theme);
const marginConfig = getMargins(margins);
const html = marked.parse(content);
this.super.handlerProps.log(
`create-docx-file: Parsed markdown to HTML (${html.length} chars), theme: ${theme}, margins: ${margins}`
);
const logoBuffer = createFilesLib.getLogo({
forDarkBackground: false,
format: "buffer",
});
const docElements = await htmlToDocxElements(
html,
libs,
this.super.handlerProps.log,
themeColors
);
if (docElements.length === 0) {
docElements.push(
new Paragraph({
children: [new TextRun({ text: content })],
})
);
}
const sections = [];
if (includeTitlePage) {
const currentDate = new Date().toLocaleDateString("en-US", {
year: "numeric",
month: "long",
day: "numeric",
});
sections.push(
createCoverPageSection(docx, {
title: documentTitle,
subtitle,
author,
date: currentDate,
theme: themeColors,
margins: marginConfig,
logoBuffer,
})
);
sections.push({
properties: {
page: {
margin: marginConfig,
},
},
children: docElements,
headers: {
default: createRunningHeader(
docx,
documentTitle,
themeColors
),
},
footers: {
default: createRunningFooter(docx, logoBuffer, themeColors),
},
});
} else {
sections.push({
properties: {
page: {
margin: marginConfig,
},
},
children: docElements,
footers: {
default: createRunningFooter(docx, logoBuffer, themeColors),
},
});
}
const doc = new Document({
title: documentTitle,
creator: `AnythingLLM ${getDeploymentVersion()}`,
description: `Word Document generated by AnythingLLM ${getDeploymentVersion()}`,
numbering: DEFAULT_NUMBERING_CONFIG,
sections,
});
const buffer = await Packer.toBuffer(doc);
const bufferSizeKB = (buffer.length / 1024).toFixed(2);
this.super.handlerProps.log(
`create-docx-file: Generated buffer - size: ${bufferSizeKB}KB, title: "${documentTitle}", theme: ${theme}`
);
const savedFile = await createFilesLib.saveGeneratedFile({
fileType: "docx",
extension: "docx",
buffer,
displayFilename,
});
this.super.socket.send("fileDownloadCard", {
filename: savedFile.displayFilename,
storageFilename: savedFile.filename,
fileSize: savedFile.fileSize,
});
createFilesLib.registerOutput(this.super, "DocxFileDownload", {
filename: savedFile.displayFilename,
storageFilename: savedFile.filename,
fileSize: savedFile.fileSize,
});
this.super.introspect(
`${this.caller}: Successfully created Word document "${displayFilename}"`
);
const styleInfo = [
theme !== "neutral" ? `${theme} theme` : null,
margins !== "normal" ? `${margins} margins` : null,
includeTitlePage ? "title page" : null,
].filter(Boolean);
const styleDesc =
styleInfo.length > 0 ? ` with ${styleInfo.join(", ")}` : "";
return `Successfully created Word document "${displayFilename}" (${bufferSizeKB}KB)${styleDesc}. The document includes formatted content with tables, images, Page X of Y footer, and professional styling.`;
} catch (e) {
this.super.handlerProps.log(
`create-docx-file error: ${e.message}`
);
this.super.introspect(`Error: ${e.message}`);
return `Error creating Word document: ${e.message}`;
}
},
});
},
};
},
};

View File

@@ -0,0 +1,298 @@
#!/usr/bin/env node
/**
* Test utility to generate sample Word documents for all themes and configurations.
* Run from the server directory: node utils/agents/aibitat/plugins/create-files/docx/test-themes.js
*
* Output goes to: storage/generated-files/docx-theme-previews/
*/
const path = require("path");
const fs = require("fs/promises");
const {
DOCUMENT_STYLES,
getTheme,
getMargins,
loadLibraries,
htmlToDocxElements,
createCoverPageSection,
createRunningHeader,
createRunningFooter,
DEFAULT_NUMBERING_CONFIG,
} = require("./utils.js");
const OUTPUT_DIR = path.resolve(
__dirname,
"../../../../../../storage/generated-files/docx-theme-previews"
);
const SAMPLE_CONTENT = `# Sample Document
## Executive Summary
This document demonstrates the **styling capabilities** of the Word document generator. It includes various content types to showcase how themes affect the visual appearance.
## Key Features
- Professional title pages with centered content
- Running headers with document title
- Page X of Y footer numbering
- Color-coordinated themes throughout
## Data Overview
| Metric | Q1 | Q2 | Q3 | Q4 |
|--------|-----|-----|-----|-----|
| Revenue | $1.2M | $1.5M | $1.8M | $2.1M |
| Growth | +15% | +25% | +20% | +17% |
| Users | 10K | 15K | 22K | 30K |
## Technical Details
Here is an example code block:
\`\`\`javascript
const config = {
theme: "blue",
margins: "normal",
includeTitlePage: true
};
\`\`\`
> **Note:** This blockquote demonstrates how accent colors are applied to the left border. Blockquotes are useful for callouts and important notes.
## Conclusion
The themed document system provides a consistent, professional look across all generated documents. Each theme cascades colors through:
1. Heading text colors
2. Table header backgrounds
3. Blockquote borders
4. Footer text styling
---
Thank you for reviewing this sample document.
`;
const MINIMAL_CONTENT = `# Quick Report
## Summary
A brief document to test minimal content rendering.
- Point one
- Point two
- Point three
| Item | Value |
|------|-------|
| A | 100 |
| B | 200 |
`;
async function generateThemePreview(themeName, themeConfig, options = {}) {
const libs = await loadLibraries();
const { marked, docx } = libs;
const { Document, Packer, Paragraph, TextRun } = docx;
marked.setOptions({ gfm: true, breaks: true });
const {
margins = "normal",
includeTitlePage = false,
content = SAMPLE_CONTENT,
subtitle = null,
author = null,
} = options;
const marginConfig = getMargins(margins);
const title = `${themeConfig.name || themeName} Theme Preview`;
const html = marked.parse(content);
const docElements = await htmlToDocxElements(
html,
libs,
console.log,
themeConfig
);
if (docElements.length === 0) {
docElements.push(
new Paragraph({
children: [new TextRun({ text: content })],
})
);
}
const sections = [];
if (includeTitlePage) {
const currentDate = new Date().toLocaleDateString("en-US", {
year: "numeric",
month: "long",
day: "numeric",
});
sections.push(
createCoverPageSection(docx, {
title,
subtitle: subtitle || `Demonstrating the ${themeName} color scheme`,
author: author || "AnythingLLM Theme Tester",
date: currentDate,
theme: themeConfig,
margins: marginConfig,
logoBuffer: null,
})
);
sections.push({
properties: {
page: { margin: marginConfig },
titlePage: true,
},
children: docElements,
headers: {
default: createRunningHeader(docx, title, themeConfig),
},
footers: {
default: createRunningFooter(docx, null, themeConfig),
},
});
} else {
sections.push({
properties: {
page: { margin: marginConfig },
},
children: docElements,
footers: {
default: createRunningFooter(docx, null, themeConfig),
},
});
}
const doc = new Document({
title,
creator: "AnythingLLM Theme Tester",
description: `Theme preview for ${themeName}`,
numbering: DEFAULT_NUMBERING_CONFIG,
sections,
});
return Packer.toBuffer(doc);
}
async function main() {
console.log("DOCX Theme Preview Generator");
console.log("============================\n");
await fs.mkdir(OUTPUT_DIR, { recursive: true });
const themes = Object.keys(DOCUMENT_STYLES.themes);
const marginPresets = Object.keys(DOCUMENT_STYLES.margins);
console.log(`Themes: ${themes.join(", ")}`);
console.log(`Margins: ${marginPresets.join(", ")}\n`);
const configs = [];
for (const themeName of themes) {
configs.push({
name: `theme-${themeName}-simple`,
theme: themeName,
margins: "normal",
includeTitlePage: false,
content: SAMPLE_CONTENT,
});
configs.push({
name: `theme-${themeName}-with-title-page`,
theme: themeName,
margins: "normal",
includeTitlePage: true,
content: SAMPLE_CONTENT,
});
}
for (const marginName of marginPresets) {
configs.push({
name: `margins-${marginName}`,
theme: "neutral",
margins: marginName,
includeTitlePage: true,
content: SAMPLE_CONTENT,
});
}
configs.push({
name: `full-featured-blue`,
theme: "blue",
margins: "normal",
includeTitlePage: true,
content: SAMPLE_CONTENT,
subtitle: "A Complete Feature Demonstration",
author: "Documentation Team",
});
configs.push({
name: `minimal-warm`,
theme: "warm",
margins: "narrow",
includeTitlePage: false,
content: MINIMAL_CONTENT,
});
console.log(`Generating ${configs.length} preview documents...\n`);
for (const config of configs) {
const themeConfig = getTheme(config.theme);
try {
const buffer = await generateThemePreview(config.theme, themeConfig, {
margins: config.margins,
includeTitlePage: config.includeTitlePage,
content: config.content,
subtitle: config.subtitle,
author: config.author,
});
const filename = `${config.name}.docx`;
const filepath = path.join(OUTPUT_DIR, filename);
await fs.writeFile(filepath, buffer);
const sizeKB = (buffer.length / 1024).toFixed(1);
const titlePage = config.includeTitlePage ? "✓ title" : " - ";
console.log(
`${config.name.padEnd(30)} [${config.theme.padEnd(7)}] [${config.margins.padEnd(6)}] ${titlePage} (${sizeKB}KB)`
);
} catch (error) {
console.error(`${config.name.padEnd(30)} → Error: ${error.message}`);
console.error(error.stack);
}
}
console.log(`\n✅ Done! Files saved to: ${OUTPUT_DIR}`);
console.log(
"\nOpen the .docx files in Microsoft Word or LibreOffice to preview each configuration."
);
console.log("\n--- Theme Color Reference ---");
for (const [name, colors] of Object.entries(DOCUMENT_STYLES.themes)) {
console.log(`\n${name.toUpperCase()}:`);
console.log(` Heading: #${colors.heading}`);
console.log(` Accent: #${colors.accent}`);
console.log(` Table Header: #${colors.tableHeader}`);
console.log(` Border: #${colors.border}`);
console.log(` Cover BG: #${colors.coverBg}`);
console.log(` Footer Text: #${colors.footerText}`);
}
console.log("\n--- Margin Presets (twips) ---");
for (const [name, margins] of Object.entries(DOCUMENT_STYLES.margins)) {
const inchTop = (margins.top / 1440).toFixed(2);
const inchLeft = (margins.left / 1440).toFixed(2);
console.log(
`${name.padEnd(8)}: top/bottom=${inchTop}" left/right=${inchLeft}"`
);
}
}
main().catch(console.error);

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,23 @@
const { CreatePptxPresentation } = require("./pptx/create-presentation.js");
const { CreateTextFile } = require("./text/create-text-file.js");
const { CreatePdfFile } = require("./pdf/create-pdf-file.js");
const { CreateExcelFile } = require("./xlsx/create-excel-file.js");
const { CreateDocxFile } = require("./docx/create-docx-file.js");
const createFilesAgent = {
name: "create-files-agent",
startupConfig: {
params: {},
},
plugin: [
CreatePptxPresentation,
CreateTextFile,
CreatePdfFile,
CreateExcelFile,
CreateDocxFile,
],
};
module.exports = {
createFilesAgent,
};

View File

@@ -0,0 +1,298 @@
const path = require("path");
const fs = require("fs/promises");
const fsSync = require("fs");
const { v4: uuidv4 } = require("uuid");
/**
* Manages file creation operations for binary document formats.
* Handles both browser download and filesystem write modes.
* All generated files are saved to storage/generated-files directory.
*/
class CreateFilesManager {
#outputDirectory = null;
#isInitialized = false;
/**
* Gets the output directory for generated files.
* @returns {string} The output directory path (storage/generated-files)
*/
#getOutputDirectory() {
const storageRoot =
process.env.STORAGE_DIR ||
path.resolve(__dirname, "../../../../../storage");
return path.join(storageRoot, "generated-files");
}
/**
* Initializes the create-files manager and ensures output directory exists.
* @returns {Promise<string>} The output directory path
*/
async #initialize() {
this.#outputDirectory = this.#getOutputDirectory();
try {
await fs.mkdir(this.#outputDirectory, { recursive: true });
} catch (error) {
console.error(
`Warning: Could not create output directory ${this.#outputDirectory}: ${error.message}`
);
}
this.#isInitialized = true;
return this.#outputDirectory;
}
/**
* Ensures the create-files manager is initialized before use.
* @returns {Promise<void>}
*/
async ensureInitialized() {
if (!this.#isInitialized) await this.#initialize();
}
/**
* Checks if file creation tools are available.
* @returns {boolean} True if tools are available
*/
isToolAvailable() {
if (process.env.NODE_ENV === "development") return true;
return process.env.ANYTHING_LLM_RUNTIME === "docker";
}
/**
* Gets the output directory path.
* @returns {Promise<string>} The output directory path
*/
async getOutputDirectory() {
await this.ensureInitialized();
return this.#outputDirectory;
}
/**
* Writes binary content (Buffer) to a file.
* @param {string} filePath - Validated absolute path to write to
* @param {Buffer} buffer - Binary content to write
* @returns {Promise<void>}
*/
async writeBinaryFile(filePath, buffer) {
const parentDir = path.dirname(filePath);
const fileSizeBytes = buffer.length;
const fileSizeKB = (fileSizeBytes / 1024).toFixed(2);
const fileSizeMB = (fileSizeBytes / (1024 * 1024)).toFixed(2);
console.log(
`[CreateFilesManager] writeBinaryFile starting - path: ${filePath}, size: ${fileSizeKB}KB (${fileSizeMB}MB)`
);
await fs.mkdir(parentDir, { recursive: true });
await fs.writeFile(filePath, buffer);
console.log(
`[CreateFilesManager] writeBinaryFile completed - file saved to: ${filePath}`
);
}
/**
* Gets the MIME type for a file extension.
* @param {string} extension - File extension (with or without dot)
* @returns {string} MIME type
*/
getMimeType(extension) {
const ext = extension.startsWith(".") ? extension : `.${extension}`;
const mimeTypes = {
".pptx":
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
".xlsx":
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
".docx":
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
".pdf": "application/pdf",
".txt": "text/plain",
".csv": "text/csv",
".json": "application/json",
".html": "text/html",
".xml": "application/xml",
".zip": "application/zip",
".png": "image/png",
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".gif": "image/gif",
".svg": "image/svg+xml",
".mp3": "audio/mpeg",
".mp4": "video/mp4",
".webm": "video/webm",
};
return mimeTypes[ext.toLowerCase()] || "application/octet-stream";
}
/**
* Checks if a file exists.
* @param {string} filePath - Path to check
* @returns {Promise<boolean>} True if file exists
*/
async fileExists(filePath) {
try {
await fs.access(filePath);
return true;
} catch {
return false;
}
}
/**
* Reads a file as a Buffer.
* @param {string} filePath - Path to the file
* @returns {Promise<Buffer>} File content as Buffer
*/
async readBinaryFile(filePath) {
return await fs.readFile(filePath);
}
/**
* Registers an output to be persisted in the chat history.
* This allows files and other outputs to be re-rendered when viewing historical messages.
* @param {object} aibitat - The aibitat instance to register the output on
* @param {string} type - The type of output (e.g., "PptxFileDownload")
* @param {object} payload - The output payload data
*/
registerOutput(aibitat, type, payload) {
if (!aibitat) {
console.warn(
"[CreateFilesManager] Cannot register output - aibitat instance not provided"
);
return;
}
if (!aibitat._pendingOutputs) {
aibitat._pendingOutputs = [];
}
aibitat._pendingOutputs.push({ type, payload });
console.log(
`[CreateFilesManager] Registered output: type=${type}, total pending=${aibitat._pendingOutputs.length}`
);
}
/**
* Generates a standardized filename for generated files.
* Format: {fileType}-{fileUUID}.{extension}
* @param {string} fileType - Type identifier (e.g., 'pptx', 'xlsx')
* @param {string} extension - File extension (without dot)
* @returns {string} The generated filename
*/
generateFilename(fileType, extension) {
const fileUUID = uuidv4();
return `${fileType}-${fileUUID}.${extension}`;
}
/**
* Parses a generated filename to extract its components.
* @param {string} filename - The filename to parse
* @returns {{fileType: string, fileUUID: string, extension: string} | null}
*/
parseFilename(filename) {
const match = filename.match(/^([a-z]+)-([a-f0-9-]{36})\.(\w+)$/i);
if (!match) return null;
return {
fileType: match[1],
fileUUID: match[2],
extension: match[3],
};
}
/**
* Saves a generated file to storage and returns metadata for WebSocket/DB storage.
* This is the primary method for persisting agent-generated files.
* @param {object} params
* @param {string} params.fileType - Type identifier (e.g., 'pptx', 'xlsx')
* @param {string} params.extension - File extension (without dot)
* @param {Buffer} params.buffer - The file content as a Buffer
* @param {string} params.displayFilename - The user-friendly filename for display
* @returns {Promise<{filename: string, displayFilename: string, fileSize: number, storagePath: string}>}
*/
async saveGeneratedFile({ fileType, extension, buffer, displayFilename }) {
await this.ensureInitialized();
const filename = this.generateFilename(fileType, extension);
const storagePath = path.join(this.#outputDirectory, filename);
await this.writeBinaryFile(storagePath, buffer);
console.log(
`[CreateFilesManager] saveGeneratedFile - saved ${filename} (${(buffer.length / 1024).toFixed(2)}KB)`
);
return {
filename,
displayFilename,
fileSize: buffer.length,
storagePath,
};
}
/**
* Retrieves a generated file by its storage filename.
* @param {string} filename - The storage filename (must match {fileType}-{uuid}.{ext} format)
* @returns {Promise<{buffer: Buffer, storagePath: string} | null>}
*/
async getGeneratedFile(filename) {
await this.ensureInitialized();
// Defense-in-depth: validate filename format to prevent path traversal
if (!this.parseFilename(filename)) {
console.warn(
`[CreateFilesManager] getGeneratedFile - rejected invalid filename format: ${filename}`
);
return null;
}
const storagePath = path.join(this.#outputDirectory, filename);
const exists = await this.fileExists(storagePath);
if (!exists) return null;
const buffer = await this.readBinaryFile(storagePath);
return { buffer, storagePath };
}
/**
* Sanitizes a filename for use in Content-Disposition header to prevent header injection.
* Removes/replaces characters that could be used for header manipulation.
* @param {string} filename - The filename to sanitize
* @returns {string} Sanitized filename safe for Content-Disposition header
*/
sanitizeFilenameForHeader(filename) {
if (!filename || typeof filename !== "string") return "download";
return filename
.replace(/[\r\n"\\]/g, "_")
.replace(/[^\x20-\x7E]/g, "_")
.substring(0, 255);
}
/**
* Gets the AnythingLLM logo for branding.
* @param {Object} options
* @param {boolean} [options.forDarkBackground=false] - True to get light logo (for dark backgrounds), false for dark logo (for light backgrounds)
* @param {"buffer"|"dataUri"} [options.format="buffer"] - Return format: "buffer" for raw Buffer, "dataUri" for base64 data URI
* @returns {Buffer|string|null} Logo as Buffer, data URI string, or null if file not found
*/
getLogo({ forDarkBackground = false, format = "buffer" } = {}) {
const assetsPath = path.join(__dirname, "../../../../../storage/assets");
const filename = forDarkBackground
? "anything-llm.png"
: "anything-llm-invert.png";
try {
if (format === "dataUri") {
const base64 = fsSync.readFileSync(
path.join(assetsPath, filename),
"base64"
);
return `image/png;base64,${base64}`;
}
return fsSync.readFileSync(path.join(assetsPath, filename));
} catch {
return null;
}
}
}
module.exports = new CreateFilesManager();

View File

@@ -0,0 +1,138 @@
const createFilesLib = require("../lib.js");
const { applyBranding } = require("./utils.js");
module.exports.CreatePdfFile = {
name: "create-pdf-file",
plugin: function () {
return {
name: "create-pdf-file",
setup(aibitat) {
aibitat.function({
super: aibitat,
name: this.name,
description:
"Create a PDF document from markdown or plain text content. " +
"The content will be styled and converted to a professional PDF document. " +
"Supports markdown formatting including headers, lists, code blocks, tables, and more.",
examples: [
{
prompt: "Create a PDF report about quarterly sales",
call: JSON.stringify({
filename: "quarterly-sales-report.pdf",
content:
"# Quarterly Sales Report\n\n## Q1 2024 Summary\n\n### Key Metrics\n- Total Revenue: $1.2M\n- Growth: 15% YoY\n- New Customers: 234\n\n### Top Products\n1. Product A - $400K\n2. Product B - $350K\n3. Product C - $250K\n\n## Recommendations\n\nBased on the analysis, we recommend focusing on...",
}),
},
{
prompt: "Create a PDF document with meeting minutes",
call: JSON.stringify({
filename: "meeting-minutes.pdf",
content:
"# Team Meeting Minutes\n\n**Date:** January 15, 2024\n**Attendees:** John, Sarah, Mike, Lisa\n\n## Agenda Items\n\n### 1. Project Status Update\nThe project is on track for Q2 delivery. Key milestones:\n- [ ] Phase 1 complete\n- [x] Phase 2 in progress\n- [ ] Phase 3 pending\n\n### 2. Budget Review\n| Category | Allocated | Spent |\n|----------|-----------|-------|\n| Development | $50,000 | $35,000 |\n| Marketing | $20,000 | $12,000 |\n\n### Action Items\n- John: Complete technical review by Friday\n- Sarah: Schedule stakeholder meeting",
}),
},
{
prompt: "Create a PDF with code documentation",
call: JSON.stringify({
filename: "api-documentation.pdf",
content:
"# API Documentation\n\n## Authentication\n\nAll API requests require a Bearer token:\n\n```javascript\nfetch('/api/data', {\n headers: {\n 'Authorization': 'Bearer YOUR_TOKEN'\n }\n});\n```\n\n## Endpoints\n\n### GET /api/users\n\nReturns a list of all users.\n\n**Response:**\n```json\n{\n \"users\": [...],\n \"total\": 100\n}\n```",
}),
},
],
parameters: {
$schema: "http://json-schema.org/draft-07/schema#",
type: "object",
properties: {
filename: {
type: "string",
description:
"The filename for the PDF document. The .pdf extension will be added automatically if not provided.",
},
content: {
type: "string",
description:
"The markdown or plain text content to convert to PDF. Supports full markdown syntax including headers (#, ##, ###), bold (**text**), italic (*text*), lists, code blocks, tables, and more.",
},
},
required: ["filename", "content"],
additionalProperties: false,
},
handler: async function ({
filename = "document.pdf",
content = "",
}) {
try {
this.super.handlerProps.log(`Using the create-pdf-file tool.`);
const hasExtension = /\.pdf$/i.test(filename);
if (!hasExtension) filename = `${filename}.pdf`;
if (this.super.requestToolApproval) {
const approval = await this.super.requestToolApproval({
skillName: this.name,
payload: { filename },
description: `Create PDF document "${filename}"`,
});
if (!approval.approved) {
this.super.introspect(
`${this.caller}: User rejected the ${this.name} request.`
);
return approval.message;
}
}
this.super.introspect(
`${this.caller}: Creating PDF document "${filename}"`
);
const { markdownToPdf } = await import("@mdpdf/mdpdf");
const { PDFDocument, rgb, StandardFonts } = await import(
"pdf-lib"
);
const rawBuffer = await markdownToPdf(content);
const pdfDoc = await PDFDocument.load(rawBuffer);
await applyBranding(pdfDoc, { rgb, StandardFonts });
const buffer = await pdfDoc.save();
const bufferSizeKB = (buffer.length / 1024).toFixed(2);
const displayFilename = filename.split("/").pop();
const savedFile = await createFilesLib.saveGeneratedFile({
fileType: "pdf",
extension: "pdf",
buffer,
displayFilename,
});
this.super.socket.send("fileDownloadCard", {
filename: savedFile.displayFilename,
storageFilename: savedFile.filename,
fileSize: savedFile.fileSize,
});
createFilesLib.registerOutput(this.super, "PdfFileDownload", {
filename: savedFile.displayFilename,
storageFilename: savedFile.filename,
fileSize: savedFile.fileSize,
});
this.super.introspect(
`${this.caller}: Successfully created PDF document "${displayFilename}"`
);
return `Successfully created PDF document "${displayFilename}" (${bufferSizeKB}KB).`;
} catch (e) {
this.super.handlerProps.log(
`create-pdf-file error: ${e.message}`
);
this.super.introspect(`Error: ${e.message}`);
return `Error creating PDF document: ${e.message}`;
}
},
});
},
};
},
};

View File

@@ -0,0 +1,70 @@
const createFilesLib = require("../lib.js");
/**
* Applies AnythingLLM branding to a PDF document.
* Adds a logo watermark or fallback text to the bottom-right of each page.
* @param {PDFDocument} pdfDoc - The pdf-lib PDFDocument instance
* @param {Object} pdfLib - The pdf-lib module exports (rgb, StandardFonts)
* @returns {Promise<void>}
*/
async function applyBranding(pdfDoc, { rgb, StandardFonts }) {
const font = await pdfDoc.embedFont(StandardFonts.HelveticaOblique);
const pages = pdfDoc.getPages();
const logoPng = createFilesLib.getLogo({
forDarkBackground: false,
format: "buffer",
});
const logoImage = logoPng ? await pdfDoc.embedPng(logoPng) : null;
const logoWidth = 80;
const logoHeight = logoImage
? (logoImage.height / logoImage.width) * logoWidth
: 0;
const marginRight = 20;
const marginBottom = 20;
for (const page of pages) {
const { width } = page.getSize();
if (logoImage) {
const createdWithText = "created with";
const fontSize = 7;
const textWidth = font.widthOfTextAtSize(createdWithText, fontSize);
const logoX = width - marginRight - logoWidth;
page.drawText(createdWithText, {
x: logoX + (logoWidth - textWidth) / 2,
y: marginBottom + logoHeight + 2,
size: fontSize,
font,
color: rgb(0.6, 0.6, 0.6),
opacity: 0.6,
});
page.drawImage(logoImage, {
x: logoX,
y: marginBottom,
width: logoWidth,
height: logoHeight,
opacity: 0.6,
});
} else {
const fallbackText = "Created with AnythingLLM";
const fontSize = 9;
const textWidth = font.widthOfTextAtSize(fallbackText, fontSize);
page.drawText(fallbackText, {
x: width - marginRight - textWidth,
y: marginBottom,
size: fontSize,
font,
color: rgb(0.6, 0.6, 0.6),
});
}
}
}
module.exports = {
applyBranding,
};

View File

@@ -0,0 +1,342 @@
const createFilesLib = require("../lib.js");
const { getTheme, getAvailableThemes } = require("./themes.js");
const {
renderTitleSlide,
renderSectionSlide,
renderContentSlide,
renderBlankSlide,
} = require("./utils.js");
const { runSectionAgent } = require("./section-agent.js");
/**
* Extracts recent conversation history from the parent AIbitat's chat log
* to provide context to each section sub-agent.
* @param {Array} chats - The parent AIbitat's _chats array
* @param {number} [maxMessages=10] - Maximum messages to include
* @returns {string} Formatted conversation context
*/
function extractConversationContext(chats, maxMessages = 10) {
if (!Array.isArray(chats) || chats.length === 0) return "";
const recent = chats
.filter((c) => c.state === "success" && c.content)
.slice(-maxMessages);
if (recent.length === 0) return "";
return recent
.map((c) => {
const content =
typeof c.content === "string" ? c.content.substring(0, 500) : "";
return `${c.from}: ${content}`;
})
.join("\n");
}
module.exports.CreatePptxPresentation = {
name: "create-pptx-presentation",
plugin: function () {
return {
name: "create-pptx-presentation",
setup(aibitat) {
aibitat.function({
super: aibitat,
name: this.name,
description:
"Create a professional PowerPoint presentation (PPTX). " +
"Provide a title, theme, and section outlines with key points. " +
"Each section is independently researched and built by a focused sub-agent " +
"that can use web search and web scraping to gather data.",
examples: [
{
prompt: "Create a presentation about project updates",
call: JSON.stringify({
filename: "project-updates.pptx",
title: "Q1 Project Updates",
theme: "corporate",
sections: [
{
title: "Overview",
keyPoints: [
"Project on track for Q1 delivery",
"Team expanded by 2 new members",
"Budget within expectations",
],
},
{
title: "Key Achievements",
keyPoints: [
"Launched new feature X",
"Reduced bug count by 40%",
"Improved performance by 25%",
],
instructions:
"Include specific metrics and quarter-over-quarter comparisons",
},
],
}),
},
{
prompt: "Create a dark themed presentation about AI trends",
call: JSON.stringify({
filename: "ai-trends.pptx",
title: "AI Trends 2025",
theme: "dark",
sections: [
{
title: "Large Language Models",
keyPoints: [
"Model scaling trends",
"Open vs closed source landscape",
],
instructions:
"Research the latest developments and include recent data",
},
{
title: "AI in Enterprise",
keyPoints: ["Adoption rates", "Top use cases", "ROI data"],
},
],
}),
},
],
parameters: {
$schema: "http://json-schema.org/draft-07/schema#",
type: "object",
properties: {
filename: {
type: "string",
description:
"The filename for the presentation (should end with .pptx).",
},
title: {
type: "string",
description:
"The title of the presentation (shown on title slide).",
},
author: {
type: "string",
description:
"Optional author name for the presentation metadata.",
},
theme: {
type: "string",
enum: getAvailableThemes(),
description:
"Color theme for the presentation. Options: " +
getAvailableThemes().join(", "),
},
sections: {
type: "array",
description:
"Section outlines for the presentation. Each section is independently researched and built by a focused sub-agent.",
items: {
type: "object",
properties: {
title: {
type: "string",
description: "The section title.",
},
keyPoints: {
type: "array",
items: { type: "string" },
description:
"Key points this section should cover. The sub-agent will expand these into detailed slides.",
},
instructions: {
type: "string",
description:
"Optional guidance for the section builder (e.g. 'research recent statistics', 'compare with competitors', 'include a data table').",
},
},
required: ["title"],
},
},
},
required: ["filename", "title", "sections"],
additionalProperties: false,
},
handler: async function ({
filename = "presentation.pptx",
title = "Untitled Presentation",
author = "",
theme: themeName = "default",
sections = [],
}) {
try {
this.super.handlerProps.log(
`Using the create-pptx-presentation tool.`
);
if (!filename.toLowerCase().endsWith(".pptx"))
filename += ".pptx";
const theme = getTheme(themeName);
const totalSections = sections.length;
this.super.introspect(
`${this.caller}: Planning presentation "${title}" — ${totalSections} section${totalSections !== 1 ? "s" : ""}, ${theme.name} theme`
);
// Ask for approval BEFORE kicking off the expensive sub-agent work
if (this.super.requestToolApproval) {
const approval = await this.super.requestToolApproval({
skillName: this.name,
payload: {
filename,
title,
sectionCount: totalSections,
sectionTitles: sections.map((s) => s.title),
},
description: `Create PowerPoint presentation "${title}" with ${totalSections} sections`,
});
if (!approval.approved) {
this.super.introspect(
`${this.caller}: User rejected the ${this.name} request.`
);
return approval.message;
}
}
const conversationContext = extractConversationContext(
this.super._chats
);
// Run a focused sub-agent for each section sequentially.
// Sequential execution is intentional — local models typically serve
// one request at a time, and it keeps introspection events ordered.
const allSlides = [];
const allCitations = [];
for (let i = 0; i < sections.length; i++) {
const section = sections[i];
this.super.introspect(
`${this.caller}: [${i + 1}/${totalSections}] Building section "${section.title}"…`
);
const sectionResult = await runSectionAgent({
parentAibitat: this.super,
section,
presentationTitle: title,
conversationContext,
sectionPrefix: `${i + 1}/${totalSections}`,
});
const slideCount = sectionResult.slides?.length || 0;
allSlides.push(...(sectionResult.slides || []));
if (sectionResult.citations?.length > 0)
allCitations.push(...sectionResult.citations);
this.super.introspect(
`${this.caller}: [${i + 1}/${totalSections}] Section "${section.title}" complete — ${slideCount} slide${slideCount !== 1 ? "s" : ""}`
);
}
// Roll up all citations from sub-agents to the parent so they
// appear as sources on the final assistant message.
if (allCitations.length > 0) this.super.addCitation(allCitations);
// Assemble the final PPTX from all section outputs
this.super.introspect(
`${this.caller}: Assembling final deck — ${allSlides.length} slides total`
);
const PptxGenJS = require("pptxgenjs");
const pptx = new PptxGenJS();
pptx.title = title;
if (author) pptx.author = author;
pptx.company = "AnythingLLM";
const totalSlideCount = allSlides.length;
// Title slide
const titleSlide = pptx.addSlide();
renderTitleSlide(titleSlide, pptx, { title, author }, theme);
// Render every slide produced by the section agents
allSlides.forEach((slideData, index) => {
const slide = pptx.addSlide();
const slideNumber = index + 1;
const layout = slideData.layout || "content";
switch (layout) {
case "title":
case "section":
renderSectionSlide(
slide,
pptx,
slideData,
theme,
slideNumber,
totalSlideCount
);
break;
case "blank":
renderBlankSlide(
slide,
pptx,
theme,
slideNumber,
totalSlideCount
);
break;
default:
renderContentSlide(
slide,
pptx,
slideData,
theme,
slideNumber,
totalSlideCount
);
break;
}
});
const buffer = await pptx.write({ outputType: "nodebuffer" });
const bufferSizeKB = (buffer.length / 1024).toFixed(2);
const bufferSizeMB = (buffer.length / (1024 * 1024)).toFixed(2);
this.super.handlerProps.log(
`create-pptx-presentation: Generated buffer - size: ${bufferSizeKB}KB (${bufferSizeMB}MB), slides: ${totalSlideCount}, theme: ${theme.name}`
);
const displayFilename = filename.split("/").pop();
const savedFile = await createFilesLib.saveGeneratedFile({
fileType: "pptx",
extension: "pptx",
buffer,
displayFilename,
});
this.super.socket.send("fileDownloadCard", {
filename: savedFile.displayFilename,
storageFilename: savedFile.filename,
fileSize: savedFile.fileSize,
});
createFilesLib.registerOutput(this.super, "PptxFileDownload", {
filename: savedFile.displayFilename,
storageFilename: savedFile.filename,
fileSize: savedFile.fileSize,
});
this.super.introspect(
`${this.caller}: Successfully created presentation "${title}"`
);
return `Successfully created presentation "${title}" with ${totalSlideCount} slides across ${totalSections} sections using the ${theme.name} theme.`;
} catch (e) {
this.super.handlerProps.log(
`create-pptx-presentation error: ${e.message}`
);
this.super.introspect(`Error: ${e.message}`);
return `Error creating presentation: ${e.message}`;
}
},
});
},
};
},
};

View File

@@ -0,0 +1,257 @@
const AIbitat = require("../../../index.js");
const SECTION_BUILDER_PROMPT = `You are a focused presentation section builder. Your ONLY task is to create detailed slides for ONE section of a PowerPoint presentation.
You have access to web search and web scraping tools, but only use them when the topic genuinely requires up-to-date information you don't already know (e.g., current statistics, recent events, specific company data). For general knowledge topics, create slides directly from your existing knowledge.
RULES:
- Create 2-5 slides for this section (no more)
- Each content slide should have 3-6 concise bullet points
- Be specific and data-driven when possible
- Include speaker notes with key talking points
- Do NOT add a title slide - only section content
When finished, you MUST call the submit-section-slides tool with your slides. Do not respond with raw JSON - always use the tool.
Available slide layouts:
- "section": Divider slide with title + optional subtitle
- "content": Bullet points with title + content array + optional notes
- May include "table": { "headers": ["Col1", "Col2"], "rows": [["a", "b"]] }
- "blank": Empty slide`;
/**
* Spawns a focused child AIbitat agent to build slides for a single presentation section.
* The child reuses the parent's provider/model/socket so introspection events (tool calls,
* research progress) flow to the frontend in real-time.
*
* @param {Object} options
* @param {AIbitat} options.parentAibitat - The parent AIbitat instance (provides provider, socket, introspect)
* @param {Object} options.section - Section definition { title, keyPoints?, instructions? }
* @param {string} options.presentationTitle - Overall presentation title for context
* @param {string} [options.conversationContext] - Recent conversation history for context
* @param {string} [options.sectionPrefix] - Progress indicator like "1/5" for UI display
* @returns {Promise<{slides: Object[], citations: Object[]}>} Parsed section slides and accumulated citations
*/
async function runSectionAgent({
parentAibitat,
section,
presentationTitle,
conversationContext = "",
sectionPrefix = "",
}) {
const log = parentAibitat.handlerProps?.log || console.log;
const childAibitat = new AIbitat({
provider: parentAibitat.defaultProvider.provider,
model: parentAibitat.defaultProvider.model,
chats: [],
handlerProps: parentAibitat.handlerProps,
maxToolCalls: 5,
});
// Share introspect so tool activity (web-search status, etc.) streams to the frontend
childAibitat.introspect = parentAibitat.introspect;
// Filtered socket: pass through introspection but suppress reportStreamEvent
// so sub-agent chatter doesn't render in the UI as a chat message.
childAibitat.socket = {
send: (type, content) => {
if (type === "reportStreamEvent") return;
parentAibitat.socket?.send(type, content);
},
};
// Only load the research tools this sub-agent needs
const { webBrowsing } = require("../../web-browsing.js");
const { webScraping } = require("../../web-scraping.js");
childAibitat.use(webBrowsing.plugin());
childAibitat.use(webScraping.plugin());
// Internal tool for structured slide submission - not exposed as a public plugin
childAibitat.function({
super: childAibitat,
name: "submit-section-slides",
description:
"Submit the completed slides for this presentation section. Call this tool when you have finished creating all slides.",
parameters: {
$schema: "http://json-schema.org/draft-07/schema#",
type: "object",
properties: {
slides: {
type: "array",
description: "Array of slide objects for this section",
items: {
type: "object",
properties: {
layout: {
type: "string",
enum: ["section", "content", "blank"],
description: "The slide layout type",
},
title: {
type: "string",
description: "The slide title",
},
subtitle: {
type: "string",
description: "Optional subtitle (for section layout)",
},
content: {
type: "array",
items: { type: "string" },
description: "Bullet points (for content layout)",
},
notes: {
type: "string",
description: "Speaker notes for this slide",
},
table: {
type: "object",
description: "Optional table data",
properties: {
headers: {
type: "array",
items: { type: "string" },
},
rows: {
type: "array",
items: {
type: "array",
items: { type: "string" },
},
},
},
},
},
required: ["layout", "title"],
},
},
},
required: ["slides"],
additionalProperties: false,
},
handler: function ({ slides }) {
this.super._submittedSlides = slides;
return "Slides submitted successfully. Section complete.";
},
});
const functions = Array.from(childAibitat.functions.values());
const messages = [
{ role: "system", content: SECTION_BUILDER_PROMPT },
{
role: "user",
content: buildSectionPrompt({
section,
presentationTitle,
conversationContext,
}),
},
];
const provider = childAibitat.getProviderForConfig(
childAibitat.defaultProvider
);
provider.attachHandlerProps(childAibitat.handlerProps);
log(
`[SectionAgent] Running sub-agent for section: "${section.title}" with ${functions.length} tools`
);
let agentName = `@section-builder`;
if (sectionPrefix) agentName = `[${sectionPrefix}] ${agentName}`;
try {
if (provider.supportsAgentStreaming) {
await childAibitat.handleAsyncExecution(
provider,
messages,
functions,
agentName
);
} else {
await childAibitat.handleExecution(
provider,
messages,
functions,
agentName
);
}
} catch (error) {
log(`[SectionAgent] Error in section "${section.title}": ${error.message}`);
return { ...buildFallbackSlides(section), citations: [] };
}
// Collect any citations the child accumulated (from web-search, web-scrape, etc.)
const citations = childAibitat._pendingCitations || [];
// Retrieve slides from the tool call (structured data, no parsing needed)
const slides = childAibitat._submittedSlides;
if (!Array.isArray(slides) || slides.length === 0) {
log(
`[SectionAgent] No slides submitted for "${section.title}", using fallback`
);
return { ...buildFallbackSlides(section), citations };
}
log(
`[SectionAgent] Section "${section.title}" produced ${slides.length} slides, ${citations.length} citations`
);
return { slides, citations };
}
function buildSectionPrompt({
section,
presentationTitle,
conversationContext,
}) {
const parts = [
`Build slides for this section of the presentation "${presentationTitle}":`,
`\nSection Title: ${section.title}`,
];
if (section.keyPoints?.length > 0) {
parts.push(
`\nKey Points to Cover:\n${section.keyPoints.map((p) => `- ${p}`).join("\n")}`
);
}
if (section.instructions) {
parts.push(`\nSpecial Instructions: ${section.instructions}`);
}
if (conversationContext) {
parts.push(`\nContext from the conversation:\n${conversationContext}`);
}
parts.push(
`\nCreate 2-5 detailed slides and submit them using the submit-section-slides tool. Only use web search/scraping if you genuinely lack the information needed.`
);
return parts.join("\n");
}
/**
* Generates basic slides from the section definition when the sub-agent fails.
*/
function buildFallbackSlides(section) {
const slides = [
{
layout: "section",
title: section.title,
subtitle: section.subtitle || "",
},
];
if (section.keyPoints?.length > 0) {
slides.push({
layout: "content",
title: section.title,
content: section.keyPoints,
notes: `Key points for ${section.title}`,
});
}
return { slides };
}
module.exports = { runSectionAgent };

View File

@@ -0,0 +1,143 @@
/**
* Generate a preview presentation for every theme using the same rendering
* pipeline as the production tool. Run from repo root:
*
* node server/utils/agents/aibitat/plugins/create-files/pptx/test-themes.js
*
* Output → storage/generated-files/theme-previews/
*/
const path = require("path");
const fs = require("fs");
const PptxGenJS = require("pptxgenjs");
const createFilesLib = require("../lib.js");
const { getTheme, getAvailableThemes } = require("./themes.js");
const {
renderTitleSlide,
renderSectionSlide,
renderContentSlide,
renderBlankSlide,
} = require("./utils.js");
const SAMPLE_SLIDES = [
{
title: "Executive Summary",
content: [
"Revenue grew 23% year-over-year to $4.2B",
"Operating margin expanded 180bps to 28.4%",
"Customer retention rate improved to 94.7%",
"Three strategic acquisitions completed in Q3",
],
notes: "Emphasize the margin expansion story",
},
{
layout: "section",
title: "Strategic Priorities",
subtitle: "Key initiatives for the next fiscal year",
},
{
title: "Market Opportunity",
subtitle: "Total addressable market analysis",
content: [
"Global TAM estimated at $180B by 2027",
"Our serviceable market represents $42B opportunity",
"Current market share: 8.3% with clear path to 15%",
"Three adjacent markets identified for expansion",
"Competitive moat strengthening through R&D investment",
],
},
{
title: "Financial Performance",
table: {
headers: ["Metric", "FY2024", "FY2025", "Growth"],
rows: [
["Revenue", "$3.4B", "$4.2B", "+23%"],
["Gross Margin", "62.1%", "64.8%", "+270bps"],
["Operating Income", "$910M", "$1.19B", "+31%"],
["Free Cash Flow", "$780M", "$1.02B", "+31%"],
],
},
},
{
title: "Next Steps & Timeline",
content: [
"Q1: Launch Phase 2 of platform modernization",
"Q2: Complete integration of acquired entities",
"Q3: Enter two new geographic markets",
"Q4: Achieve $5B annual revenue run-rate",
],
},
];
async function generateThemePreview(themeName, outputDir) {
const theme = getTheme(themeName);
const pptx = new PptxGenJS();
pptx.title = `${theme.name} Theme Preview`;
pptx.author = "AnythingLLM";
pptx.company = "AnythingLLM";
const totalSlides = SAMPLE_SLIDES.length;
const titleSlide = pptx.addSlide();
renderTitleSlide(
titleSlide,
pptx,
{ title: `${theme.name} Theme`, author: "AnythingLLM Theme Preview" },
theme
);
SAMPLE_SLIDES.forEach((slideData, index) => {
const slide = pptx.addSlide();
const slideNumber = index + 1;
const layout = slideData.layout || "content";
switch (layout) {
case "title":
case "section":
renderSectionSlide(
slide,
pptx,
slideData,
theme,
slideNumber,
totalSlides
);
break;
case "blank":
renderBlankSlide(slide, pptx, theme, slideNumber, totalSlides);
break;
default:
renderContentSlide(
slide,
pptx,
slideData,
theme,
slideNumber,
totalSlides
);
break;
}
});
const filename = `theme-preview-${themeName}.pptx`;
const filepath = path.join(outputDir, filename);
await pptx.writeFile({ fileName: filepath });
console.log(`${theme.name}${filename}`);
}
async function main() {
const baseDir = await createFilesLib.getOutputDirectory();
const outputDir = path.join(baseDir, "theme-previews");
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
}
console.log("Generating theme previews…\n");
const themes = getAvailableThemes();
for (const themeName of themes) {
await generateThemePreview(themeName, outputDir);
}
console.log(`\nDone! ${themes.length} previews saved to:\n ${outputDir}`);
}
main().catch(console.error);

View File

@@ -0,0 +1,181 @@
/**
* Curated presentation themes for pptxgenjs.
*
* Each theme is a complete design system: title-slide palette, content-slide
* palette, table styling, footer colors, and typography. The rendering code
* in utils.js consumes these tokens to produce consistent, professional slides.
*
* Themes: default · corporate · dark · minimal · creative
*/
const THEMES = {
default: {
name: "Professional",
description: "Clean and versatile — works for any presentation",
titleSlideBackground: "1E293B",
titleSlideTitleColor: "FFFFFF",
titleSlideSubtitleColor: "94A3B8",
titleSlideAccentColor: "3B82F6",
background: "FFFFFF",
titleColor: "0F172A",
subtitleColor: "64748B",
bodyColor: "334155",
accentColor: "2563EB",
bulletColor: "2563EB",
tableHeaderBg: "1E293B",
tableHeaderColor: "FFFFFF",
tableAltRowBg: "F8FAFC",
tableBorderColor: "E2E8F0",
footerColor: "94A3B8",
footerLineColor: "E2E8F0",
fontTitle: "Calibri",
fontBody: "Calibri",
},
corporate: {
name: "Corporate",
description: "Refined and authoritative — ideal for business and finance",
titleSlideBackground: "0C1929",
titleSlideTitleColor: "FFFFFF",
titleSlideSubtitleColor: "7B96B5",
titleSlideAccentColor: "C9943E",
background: "FFFFFF",
titleColor: "0C1929",
subtitleColor: "5A6D82",
bodyColor: "2C3E50",
accentColor: "1A5276",
bulletColor: "1A5276",
tableHeaderBg: "0C1929",
tableHeaderColor: "FFFFFF",
tableAltRowBg: "F4F7FA",
tableBorderColor: "D5DBE2",
footerColor: "8B9DB3",
footerLineColor: "D5DBE2",
fontTitle: "Calibri",
fontBody: "Calibri",
},
dark: {
name: "Dark",
description: "Sleek dark theme — great for tech and product presentations",
titleSlideBackground: "0F0F1A",
titleSlideTitleColor: "F8FAFC",
titleSlideSubtitleColor: "7C8DB5",
titleSlideAccentColor: "818CF8",
background: "18181B",
titleColor: "F4F4F5",
subtitleColor: "A1A1AA",
bodyColor: "D4D4D8",
accentColor: "6366F1",
bulletColor: "818CF8",
tableHeaderBg: "6366F1",
tableHeaderColor: "FFFFFF",
tableAltRowBg: "1F1F24",
tableBorderColor: "3F3F46",
footerColor: "71717A",
footerLineColor: "3F3F46",
fontTitle: "Calibri",
fontBody: "Calibri",
},
minimal: {
name: "Minimal",
description: "Ultra-clean with maximum whitespace — lets content speak",
titleSlideBackground: "F5F5F5",
titleSlideTitleColor: "171717",
titleSlideSubtitleColor: "737373",
titleSlideAccentColor: "A3A3A3",
background: "FFFFFF",
titleColor: "171717",
subtitleColor: "737373",
bodyColor: "404040",
accentColor: "525252",
bulletColor: "A3A3A3",
tableHeaderBg: "262626",
tableHeaderColor: "FFFFFF",
tableAltRowBg: "FAFAFA",
tableBorderColor: "E5E5E5",
footerColor: "A3A3A3",
footerLineColor: "E5E5E5",
fontTitle: "Calibri",
fontBody: "Calibri Light",
},
creative: {
name: "Creative",
description: "Bold and expressive — perfect for pitches and creative work",
titleSlideBackground: "2E1065",
titleSlideTitleColor: "FFFFFF",
titleSlideSubtitleColor: "C4B5FD",
titleSlideAccentColor: "A78BFA",
background: "FFFFFF",
titleColor: "3B0764",
subtitleColor: "7C3AED",
bodyColor: "374151",
accentColor: "7C3AED",
bulletColor: "7C3AED",
tableHeaderBg: "5B21B6",
tableHeaderColor: "FFFFFF",
tableAltRowBg: "FAF5FF",
tableBorderColor: "E9D5FF",
footerColor: "A78BFA",
footerLineColor: "E9D5FF",
fontTitle: "Calibri",
fontBody: "Calibri",
},
};
/**
* Get a theme by name, falling back to default if not found.
* @param {string} themeName
* @returns {object} Theme configuration
*/
function getTheme(themeName) {
const key = (themeName || "default").toLowerCase().trim();
return THEMES[key] || THEMES.default;
}
/**
* @returns {string[]} Available theme identifiers
*/
function getAvailableThemes() {
return Object.keys(THEMES);
}
/**
* @returns {object[]} Array of { id, name, description } for documentation
*/
function getThemeDescriptions() {
return Object.entries(THEMES).map(([id, t]) => ({
id,
name: t.name,
description: t.description,
}));
}
module.exports = { THEMES, getTheme, getAvailableThemes, getThemeDescriptions };

View File

@@ -0,0 +1,378 @@
const createFilesLib = require("../lib.js");
// All positioning assumes LAYOUT_16x9: 10 × 5.625 in.
const MARGIN_X = 0.7;
const CONTENT_W = 8.6; // 10 - 2 × MARGIN_X
const SLIDE_H = 5.625;
function isDarkColor(hexColor) {
const hex = (hexColor || "FFFFFF").replace("#", "");
const r = parseInt(hex.substr(0, 2), 16);
const g = parseInt(hex.substr(2, 2), 16);
const b = parseInt(hex.substr(4, 2), 16);
return (0.299 * r + 0.587 * g + 0.114 * b) / 255 < 0.5;
}
function addBranding(slide, bgColor) {
const isDark = isDarkColor(bgColor);
const textColor = isDark ? "FFFFFF" : "000000";
const logo = createFilesLib.getLogo({
forDarkBackground: isDark,
format: "dataUri",
});
slide.addText("Created with", {
x: 7.85,
y: 5.06,
w: 1.85,
h: 0.12,
fontSize: 5.5,
color: textColor,
transparency: 78,
fontFace: "Calibri",
align: "center",
italic: true,
});
if (logo) {
slide.addImage({
data: logo,
x: 8.025,
y: 5.17,
w: 1.5,
h: 0.24,
transparency: 78,
});
} else {
slide.addText("AnythingLLM", {
x: 7.85,
y: 5.17,
w: 1.85,
h: 0.24,
fontSize: 8,
color: textColor,
transparency: 78,
fontFace: "Calibri",
align: "center",
});
}
}
function addTopAccentBar(slide, pptx, theme) {
slide.addShape(pptx.ShapeType.rect, {
x: 0,
y: 0,
w: "100%",
h: 0.05,
fill: { color: theme.accentColor },
line: { color: theme.accentColor },
});
}
function addAccentUnderline(slide, pptx, x, y, color) {
slide.addShape(pptx.ShapeType.rect, {
x,
y,
w: 1.5,
h: 0.035,
fill: { color },
line: { color },
});
}
function addSlideFooter(slide, pptx, theme, slideNumber, totalSlides) {
slide.addShape(pptx.ShapeType.rect, {
x: MARGIN_X,
y: 5.0,
w: CONTENT_W,
h: 0.007,
fill: { color: theme.footerLineColor },
line: { color: theme.footerLineColor },
});
slide.addText(`${slideNumber} / ${totalSlides}`, {
x: MARGIN_X,
y: 5.07,
w: 1.2,
h: 0.25,
fontSize: 8,
color: theme.footerColor,
fontFace: theme.fontBody,
align: "left",
});
}
function renderTitleSlide(slide, pptx, { title, author }, theme) {
slide.background = { color: theme.titleSlideBackground };
slide.addText(title || "Untitled", {
x: 1.0,
y: 1.3,
w: 8.0,
h: 1.4,
fontSize: 36,
bold: true,
color: theme.titleSlideTitleColor,
fontFace: theme.fontTitle,
align: "center",
valign: "bottom",
});
addAccentUnderline(slide, pptx, 4.25, 2.9, theme.titleSlideAccentColor);
if (author) {
slide.addText(author, {
x: 1.5,
y: 3.15,
w: 7.0,
h: 0.45,
fontSize: 14,
color: theme.titleSlideSubtitleColor,
fontFace: theme.fontBody,
align: "center",
italic: true,
});
}
// Bottom accent strip
slide.addShape(pptx.ShapeType.rect, {
x: 0,
y: SLIDE_H - 0.1,
w: "100%",
h: 0.1,
fill: { color: theme.titleSlideAccentColor },
line: { color: theme.titleSlideAccentColor },
});
addBranding(slide, theme.titleSlideBackground);
}
function renderSectionSlide(
slide,
pptx,
slideData,
theme,
slideNumber,
totalSlides
) {
slide.background = { color: theme.titleSlideBackground };
slide.addText(slideData.title || "", {
x: 1.0,
y: 1.5,
w: 8.0,
h: 1.2,
fontSize: 32,
bold: true,
color: theme.titleSlideTitleColor,
fontFace: theme.fontTitle,
align: "center",
valign: "bottom",
});
addAccentUnderline(slide, pptx, 4.25, 2.9, theme.titleSlideAccentColor);
if (slideData.subtitle) {
slide.addText(slideData.subtitle, {
x: 1.5,
y: 3.1,
w: 7.0,
h: 0.5,
fontSize: 16,
color: theme.titleSlideSubtitleColor,
fontFace: theme.fontBody,
align: "center",
});
}
const numColor = isDarkColor(theme.titleSlideBackground)
? "FFFFFF"
: "000000";
slide.addText(`${slideNumber} / ${totalSlides}`, {
x: MARGIN_X,
y: 5.1,
w: 1.2,
h: 0.25,
fontSize: 8,
color: numColor,
transparency: 65,
fontFace: theme.fontBody,
align: "left",
});
addBranding(slide, theme.titleSlideBackground);
if (slideData.notes) slide.addNotes(slideData.notes);
}
function renderContentSlide(
slide,
pptx,
slideData,
theme,
slideNumber,
totalSlides
) {
slide.background = { color: theme.background };
addTopAccentBar(slide, pptx, theme);
let contentStartY = 0.4;
if (slideData.title) {
slide.addText(slideData.title, {
x: MARGIN_X,
y: 0.3,
w: CONTENT_W,
h: 0.65,
fontSize: 24,
bold: true,
color: theme.titleColor,
fontFace: theme.fontTitle,
valign: "bottom",
});
contentStartY = 1.0;
if (slideData.subtitle) {
slide.addText(slideData.subtitle, {
x: MARGIN_X,
y: 1.0,
w: CONTENT_W,
h: 0.3,
fontSize: 13,
color: theme.subtitleColor,
fontFace: theme.fontBody,
});
contentStartY = 1.35;
}
addAccentUnderline(
slide,
pptx,
MARGIN_X,
contentStartY + 0.05,
theme.accentColor
);
contentStartY += 0.25;
}
const footerY = 5.0;
const contentHeight = footerY - contentStartY - 0.15;
if (slideData.table) {
addTableContent(slide, pptx, slideData.table, theme, contentStartY);
} else {
addBulletContent(
slide,
slideData.content,
theme,
contentStartY,
contentHeight
);
}
addSlideFooter(slide, pptx, theme, slideNumber, totalSlides);
addBranding(slide, theme.background);
if (slideData.notes) slide.addNotes(slideData.notes);
}
function renderBlankSlide(slide, pptx, theme, slideNumber, totalSlides) {
slide.background = { color: theme.background };
addSlideFooter(slide, pptx, theme, slideNumber, totalSlides);
addBranding(slide, theme.background);
}
function addBulletContent(slide, content, theme, startY, maxHeight) {
if (!Array.isArray(content) || content.length === 0) return;
const bulletPoints = content.map((text) => ({
text,
options: {
fontSize: 15,
color: theme.bodyColor,
fontFace: theme.fontBody,
bullet: { code: "25AA", color: theme.bulletColor },
paraSpaceAfter: 10,
},
}));
slide.addText(bulletPoints, {
x: MARGIN_X,
y: startY,
w: CONTENT_W,
h: maxHeight,
valign: "top",
});
}
function addTableContent(slide, pptx, tableData, theme, startY) {
if (!tableData) return;
const rows = [];
if (tableData.headers?.length > 0) {
rows.push(
tableData.headers.map((header) => ({
text: header,
options: {
bold: true,
fontSize: 12,
fontFace: theme.fontBody,
color: theme.tableHeaderColor,
fill: { color: theme.tableHeaderBg },
align: "left",
valign: "middle",
margin: [4, 8, 4, 8],
},
}))
);
}
if (tableData.rows?.length > 0) {
tableData.rows.forEach((row, idx) => {
rows.push(
row.map((cell) => ({
text: cell,
options: {
fontSize: 11,
fontFace: theme.fontBody,
color: theme.bodyColor,
fill: {
color: idx % 2 === 1 ? theme.tableAltRowBg : theme.background,
},
align: "left",
valign: "middle",
margin: [4, 8, 4, 8],
},
}))
);
});
}
if (rows.length === 0) return;
const colCount = rows[0].length;
slide.addTable(rows, {
x: MARGIN_X,
y: startY,
w: CONTENT_W,
colW: CONTENT_W / colCount,
rowH: 0.4,
border: { type: "solid", pt: 0.5, color: theme.tableBorderColor },
});
}
module.exports = {
isDarkColor,
addBranding,
addTopAccentBar,
addAccentUnderline,
addSlideFooter,
renderTitleSlide,
renderSectionSlide,
renderContentSlide,
renderBlankSlide,
addBulletContent,
addTableContent,
};

View File

@@ -0,0 +1,158 @@
const createFilesLib = require("../lib.js");
module.exports.CreateTextFile = {
name: "create-text-file",
plugin: function () {
return {
name: "create-text-file",
setup(aibitat) {
aibitat.function({
super: aibitat,
name: this.name,
description:
"Create a text file with arbitrary content. " +
"Provide the content and an optional file extension (defaults to .txt). " +
"Common extensions include .txt, .md, .json, .csv, .html, .xml, .yaml, .log, etc.",
examples: [
{
prompt: "Create a text file with meeting notes",
call: JSON.stringify({
filename: "meeting-notes.txt",
content:
"Meeting Notes - Q1 Planning\n\nAttendees: John, Sarah, Mike\n\nAgenda:\n1. Review Q4 results\n2. Set Q1 goals\n3. Assign tasks\n\nAction Items:\n- John: Prepare budget report\n- Sarah: Draft marketing plan\n- Mike: Schedule follow-up meeting",
}),
},
{
prompt: "Create a markdown file with project documentation",
call: JSON.stringify({
filename: "README.md",
extension: "md",
content:
"# Project Name\n\n## Overview\nThis project provides...\n\n## Installation\n```bash\nnpm install\n```\n\n## Usage\nRun the application with...",
}),
},
{
prompt: "Create a JSON configuration file",
call: JSON.stringify({
filename: "config.json",
extension: "json",
content: JSON.stringify(
{
appName: "MyApp",
version: "1.0.0",
settings: {
debug: false,
maxConnections: 100,
},
},
null,
2
),
}),
},
],
parameters: {
$schema: "http://json-schema.org/draft-07/schema#",
type: "object",
properties: {
filename: {
type: "string",
description:
"The filename for the text file. If no extension is provided, the extension parameter will be used (defaults to .txt).",
},
extension: {
type: "string",
description:
"The file extension to use (without the dot). Defaults to 'txt'. Common options: txt, md, json, csv, html, xml, yaml, log, etc.",
default: "txt",
},
content: {
type: "string",
description:
"The text content to write to the file. Can be any arbitrary text including multi-line content.",
},
},
required: ["filename", "content"],
additionalProperties: false,
},
handler: async function ({
filename = "document.txt",
extension = "txt",
content = "",
}) {
try {
this.super.handlerProps.log(`Using the create-text-file tool.`);
const normalizedExt = extension.toLowerCase().replace(/^\./, "");
const hasExtension = /\.\w+$/.test(filename);
if (!hasExtension) {
filename = `${filename}.${normalizedExt}`;
}
const finalExtension = filename.split(".").pop().toLowerCase();
this.super.introspect(
`${this.caller}: Creating text file "${filename}"`
);
const buffer = Buffer.from(content, "utf-8");
const bufferSizeKB = (buffer.length / 1024).toFixed(2);
this.super.handlerProps.log(
`create-text-file: Generated buffer - size: ${bufferSizeKB}KB, extension: ${finalExtension}`
);
if (this.super.requestToolApproval) {
const approval = await this.super.requestToolApproval({
skillName: this.name,
payload: { filename, extension: finalExtension },
description: `Create text file "${filename}"`,
});
if (!approval.approved) {
this.super.introspect(
`${this.caller}: User rejected the ${this.name} request.`
);
return approval.message;
}
}
const displayFilename = filename.split("/").pop();
const savedFile = await createFilesLib.saveGeneratedFile({
fileType: "text",
extension: finalExtension,
buffer,
displayFilename,
});
this.super.socket.send("fileDownloadCard", {
filename: savedFile.displayFilename,
storageFilename: savedFile.filename,
fileSize: savedFile.fileSize,
});
createFilesLib.registerOutput(this.super, "TextFileDownload", {
filename: savedFile.displayFilename,
storageFilename: savedFile.filename,
fileSize: savedFile.fileSize,
});
this.super.introspect(
`${this.caller}: Successfully created text file "${displayFilename}"`
);
return `Successfully created text file "${displayFilename}" (${bufferSizeKB}KB).`;
} catch (e) {
this.super.handlerProps.log(
`create-text-file error: ${e.message}`
);
this.super.introspect(`Error: ${e.message}`);
return `Error creating text file: ${e.message}`;
}
},
});
},
};
},
};

View File

@@ -0,0 +1,362 @@
const createFilesLib = require("../lib.js");
const {
parseCSV,
validateCSVData,
detectDelimiter,
inferCellType,
applyBranding,
autoFitColumns,
applyHeaderStyle,
applyZebraStriping,
freezePanes,
} = require("./utils.js");
module.exports.CreateExcelFile = {
name: "create-excel-file",
plugin: function () {
return {
name: "create-excel-file",
setup(aibitat) {
aibitat.function({
super: aibitat,
name: this.name,
description:
"Create an Excel spreadsheet (.xlsx) from CSV data. " +
"Supports multiple sheets, automatic type detection (numbers, dates, booleans), " +
"header styling, column auto-fit, zebra striping, and frozen panes. " +
"Provide data in CSV format with comma, semicolon, tab, or pipe delimiters.",
examples: [
{
prompt: "Create an Excel file with sales data",
call: JSON.stringify({
filename: "sales-report.xlsx",
sheets: [
{
name: "Q1 Sales",
csvData:
"Product,Region,Sales,Date\nWidget A,North,1250.50,2024-01-15\nWidget B,South,980.00,2024-01-20\nWidget C,East,1100.25,2024-02-01",
options: {
headerStyle: true,
autoFit: true,
freezeHeader: true,
},
},
],
}),
},
{
prompt: "Create a multi-sheet Excel workbook with employee data",
call: JSON.stringify({
filename: "employee-directory.xlsx",
sheets: [
{
name: "Employees",
csvData:
"ID,Name,Department,Salary,Start Date\n1,John Smith,Engineering,85000,2022-03-15\n2,Jane Doe,Marketing,72000,2021-08-01\n3,Bob Wilson,Sales,68000,2023-01-10",
options: {
headerStyle: true,
zebraStripes: true,
freezeHeader: true,
},
},
{
name: "Departments",
csvData:
"Department,Head,Budget\nEngineering,Alice Brown,500000\nMarketing,Carol White,250000\nSales,Dan Green,300000",
options: { headerStyle: true, autoFit: true },
},
],
}),
},
{
prompt: "Create a simple spreadsheet from CSV data",
call: JSON.stringify({
filename: "data-export.xlsx",
csvData:
"Name,Email,Status\nAlice,alice@example.com,Active\nBob,bob@example.com,Pending\nCharlie,charlie@example.com,Active",
}),
},
],
parameters: {
$schema: "http://json-schema.org/draft-07/schema#",
type: "object",
properties: {
filename: {
type: "string",
description:
"The filename for the Excel file. The .xlsx extension will be added automatically if not provided.",
},
csvData: {
type: "string",
description:
"CSV data for a single-sheet workbook. Use comma, semicolon, tab, or pipe as delimiter. " +
"For multiple sheets, use the 'sheets' parameter instead.",
},
sheets: {
type: "array",
description:
"Array of sheet definitions for multi-sheet workbooks. Each sheet has a name, csvData, and optional styling options.",
items: {
type: "object",
properties: {
name: {
type: "string",
description:
"The name of the worksheet (max 31 characters).",
},
csvData: {
type: "string",
description: "The CSV data for this sheet.",
},
options: {
type: "object",
description: "Optional styling options for this sheet.",
properties: {
headerStyle: {
type: "boolean",
description:
"Apply styling to the header row (bold, colored background).",
},
autoFit: {
type: "boolean",
description:
"Auto-fit column widths based on content.",
},
freezeHeader: {
type: "boolean",
description:
"Freeze the header row so it stays visible when scrolling.",
},
zebraStripes: {
type: "boolean",
description:
"Apply alternating row colors for better readability.",
},
delimiter: {
type: "string",
description:
"Override auto-detected delimiter. One of: comma, semicolon, tab, pipe.",
},
},
},
},
required: ["name", "csvData"],
},
},
options: {
type: "object",
description:
"Default styling options applied to all sheets (can be overridden per-sheet).",
properties: {
headerStyle: { type: "boolean" },
autoFit: { type: "boolean" },
freezeHeader: { type: "boolean" },
zebraStripes: { type: "boolean" },
},
},
},
required: ["filename"],
additionalProperties: false,
},
handler: async function ({
filename = "spreadsheet.xlsx",
csvData = null,
sheets = null,
options = {},
}) {
try {
this.super.handlerProps.log(`Using the create-excel-file tool.`);
const hasExtension = /\.xlsx$/i.test(filename);
if (!hasExtension) filename = `${filename}.xlsx`;
if (!csvData && (!sheets || sheets.length === 0)) {
return "Error: You must provide either 'csvData' for a single sheet or 'sheets' array for multiple sheets.";
}
const sheetDefinitions = sheets
? sheets
: [
{
name: "Sheet1",
csvData,
options: {},
},
];
for (const sheet of sheetDefinitions) {
if (!sheet.csvData || sheet.csvData.trim() === "") {
return `Error: Sheet "${sheet.name || "unnamed"}" has no CSV data.`;
}
}
const sheetCount = sheetDefinitions.length;
this.super.introspect(
`${this.caller}: Creating Excel file "${filename}" with ${sheetCount} sheet(s)`
);
if (this.super.requestToolApproval) {
const approval = await this.super.requestToolApproval({
skillName: this.name,
payload: {
filename,
sheetCount,
sheetNames: sheetDefinitions.map((s) => s.name),
},
description: `Create Excel spreadsheet "${filename}" with ${sheetCount} sheet(s)`,
});
if (!approval.approved) {
this.super.introspect(
`${this.caller}: User rejected the ${this.name} request.`
);
return approval.message;
}
}
const ExcelJS = await import("exceljs");
const workbook = new ExcelJS.default.Workbook();
workbook.creator = "AnythingLLM";
workbook.created = new Date();
workbook.modified = new Date();
const allWarnings = [];
for (const sheetDef of sheetDefinitions) {
let sheetName = (sheetDef.name || "Sheet").substring(0, 31);
sheetName = sheetName.replace(/[*?:\\/[\]]/g, "_");
const sheetOptions = {
...options,
...(sheetDef.options || {}),
};
const delimiterMap = {
comma: ",",
semicolon: ";",
tab: "\t",
pipe: "|",
};
const delimiter = sheetOptions.delimiter
? delimiterMap[sheetOptions.delimiter] ||
sheetOptions.delimiter
: detectDelimiter(sheetDef.csvData);
const parsedData = parseCSV(sheetDef.csvData, delimiter);
const validation = validateCSVData(parsedData);
if (!validation.valid) {
return `Error in sheet "${sheetName}": ${validation.error}`;
}
if (validation.warnings) {
allWarnings.push(
...validation.warnings.map((w) => `${sheetName}: ${w}`)
);
}
const worksheet = workbook.addWorksheet(sheetName);
for (
let rowIndex = 0;
rowIndex < parsedData.length;
rowIndex++
) {
const rowData = parsedData[rowIndex];
const row = worksheet.getRow(rowIndex + 1);
for (
let colIndex = 0;
colIndex < rowData.length;
colIndex++
) {
const cellValue = rowData[colIndex];
const cell = row.getCell(colIndex + 1);
const typedValue =
rowIndex === 0 ? cellValue : inferCellType(cellValue);
cell.value = typedValue;
if (typedValue instanceof Date) {
cell.numFmt = "yyyy-mm-dd";
} else if (
typeof typedValue === "number" &&
cellValue.includes("%")
) {
cell.numFmt = "0.00%";
}
}
row.commit();
}
if (sheetOptions.autoFit !== false) {
autoFitColumns(worksheet);
}
if (sheetOptions.headerStyle !== false) {
applyHeaderStyle(worksheet);
}
if (sheetOptions.zebraStripes) {
applyZebraStriping(worksheet);
}
if (sheetOptions.freezeHeader !== false) {
freezePanes(worksheet, 1, 0);
}
}
applyBranding(workbook);
const buffer = await workbook.xlsx.writeBuffer();
const bufferSizeKB = (buffer.length / 1024).toFixed(2);
const displayFilename = filename.split("/").pop();
this.super.handlerProps.log(
`create-excel-file: Generated buffer - size: ${bufferSizeKB}KB, sheets: ${sheetDefinitions.length}`
);
const savedFile = await createFilesLib.saveGeneratedFile({
fileType: "xlsx",
extension: "xlsx",
buffer: Buffer.from(buffer),
displayFilename,
});
this.super.socket.send("fileDownloadCard", {
filename: savedFile.displayFilename,
storageFilename: savedFile.filename,
fileSize: savedFile.fileSize,
});
createFilesLib.registerOutput(this.super, "ExcelFileDownload", {
filename: savedFile.displayFilename,
storageFilename: savedFile.filename,
fileSize: savedFile.fileSize,
});
this.super.introspect(
`${this.caller}: Successfully created Excel file "${displayFilename}"`
);
let result = `Successfully created Excel spreadsheet "${displayFilename}" (${bufferSizeKB}KB) with ${sheetDefinitions.length} sheet(s).`;
if (allWarnings.length > 0) {
result += `\n\nWarnings:\n${allWarnings.map((w) => `- ${w}`).join("\n")}`;
}
return result;
} catch (e) {
this.super.handlerProps.log(
`create-excel-file error: ${e.message}`
);
this.super.introspect(`Error: ${e.message}`);
return `Error creating Excel file: ${e.message}`;
}
},
});
},
};
},
};

View File

@@ -0,0 +1,333 @@
/**
* Parses CSV string into a 2D array of values.
* Handles quoted fields, embedded commas, and newlines within quotes.
* @param {string} csvString - The CSV content to parse
* @param {string} [delimiter=","] - The field delimiter
* @returns {string[][]} 2D array of parsed values
*/
function parseCSV(csvString, delimiter = ",") {
const rows = [];
let currentRow = [];
let currentField = "";
let inQuotes = false;
for (let i = 0; i < csvString.length; i++) {
const char = csvString[i];
const nextChar = csvString[i + 1];
if (inQuotes) {
if (char === '"' && nextChar === '"') {
currentField += '"';
i++;
} else if (char === '"') {
inQuotes = false;
} else {
currentField += char;
}
} else {
if (char === '"') {
inQuotes = true;
} else if (char === delimiter) {
currentRow.push(currentField.trim());
currentField = "";
} else if (char === "\r" && nextChar === "\n") {
currentRow.push(currentField.trim());
rows.push(currentRow);
currentRow = [];
currentField = "";
i++;
} else if (char === "\n" || char === "\r") {
currentRow.push(currentField.trim());
rows.push(currentRow);
currentRow = [];
currentField = "";
} else {
currentField += char;
}
}
}
if (currentField || currentRow.length > 0) {
currentRow.push(currentField.trim());
rows.push(currentRow);
}
return rows.filter((row) => row.some((cell) => cell !== ""));
}
/**
* Validates CSV data structure.
* @param {string[][]} data - Parsed CSV data
* @returns {{valid: boolean, error?: string, warnings?: string[]}}
*/
function validateCSVData(data) {
const warnings = [];
if (!data || data.length === 0) {
return { valid: false, error: "CSV data is empty" };
}
if (data.length === 1 && data[0].length === 1 && !data[0][0]) {
return { valid: false, error: "CSV data contains no meaningful content" };
}
const columnCounts = data.map((row) => row.length);
const maxColumns = Math.max(...columnCounts);
const minColumns = Math.min(...columnCounts);
if (maxColumns !== minColumns) {
warnings.push(
`Inconsistent column count: rows have between ${minColumns} and ${maxColumns} columns. Missing cells will be empty.`
);
}
if (maxColumns > 16384) {
return {
valid: false,
error: `CSV has ${maxColumns} columns, exceeding Excel's limit of 16,384 columns`,
};
}
if (data.length > 1048576) {
return {
valid: false,
error: `CSV has ${data.length} rows, exceeding Excel's limit of 1,048,576 rows`,
};
}
return { valid: true, warnings: warnings.length > 0 ? warnings : undefined };
}
/**
* Attempts to detect the delimiter used in a CSV string.
* @param {string} csvString - The CSV content
* @returns {string} Detected delimiter (comma, semicolon, tab, or pipe)
*/
function detectDelimiter(csvString) {
const firstLine = csvString.split(/\r?\n/)[0] || "";
const delimiters = [",", ";", "\t", "|"];
let bestDelimiter = ",";
let maxCount = 0;
for (const delimiter of delimiters) {
const count = (firstLine.match(new RegExp(`\\${delimiter}`, "g")) || [])
.length;
if (count > maxCount) {
maxCount = count;
bestDelimiter = delimiter;
}
}
return bestDelimiter;
}
/**
* Attempts to convert a string value to an appropriate type (number, date, boolean, or string).
* @param {string} value - The string value to convert
* @returns {string|number|Date|boolean} The converted value
*/
function inferCellType(value) {
if (value === "" || value === null || value === undefined) {
return "";
}
const trimmed = value.trim();
const lowerTrimmed = trimmed.toLowerCase();
if (lowerTrimmed === "true") return true;
if (lowerTrimmed === "false") return false;
if (/^-?\d+(\.\d+)?$/.test(trimmed)) {
const num = parseFloat(trimmed);
if (!isNaN(num) && isFinite(num)) {
return num;
}
}
if (/^-?\d{1,3}(,\d{3})*(\.\d+)?$/.test(trimmed)) {
const num = parseFloat(trimmed.replace(/,/g, ""));
if (!isNaN(num) && isFinite(num)) {
return num;
}
}
const currencyMatch = trimmed.match(/^[$€£¥₹]?\s*(-?\d+(?:[,.\d]*\d)?)\s*$/);
if (currencyMatch) {
const num = parseFloat(currencyMatch[1].replace(/,/g, ""));
if (!isNaN(num) && isFinite(num)) {
return num;
}
}
if (/^\d+(\.\d+)?%$/.test(trimmed)) {
const num = parseFloat(trimmed) / 100;
if (!isNaN(num) && isFinite(num)) {
return num;
}
}
const datePatterns = [
/^\d{4}-\d{2}-\d{2}$/,
/^\d{2}\/\d{2}\/\d{4}$/,
/^\d{2}-\d{2}-\d{4}$/,
/^\d{4}\/\d{2}\/\d{2}$/,
];
for (const pattern of datePatterns) {
if (pattern.test(trimmed)) {
const date = new Date(trimmed);
if (!isNaN(date.getTime())) {
return date;
}
}
}
return value;
}
/**
* Applies AnythingLLM branding to an Excel workbook.
* Adds a subtle "Created with AnythingLLM" text row below the data on each sheet.
* @param {import('exceljs').Workbook} workbook - The ExcelJS workbook instance
*/
function applyBranding(workbook) {
for (const worksheet of workbook.worksheets) {
const lastRow = worksheet.rowCount || 1;
const lastCol = worksheet.columnCount || 1;
const brandingRowNum = lastRow + 2;
if (lastCol > 1) {
worksheet.mergeCells(brandingRowNum, 1, brandingRowNum, lastCol);
}
const brandingCell = worksheet.getCell(brandingRowNum, 1);
brandingCell.value = "Created with AnythingLLM";
brandingCell.font = {
italic: true,
size: 9,
color: { argb: "FF999999" },
};
brandingCell.alignment = {
horizontal: "right",
vertical: "middle",
};
}
}
/**
* Auto-fits column widths based on content.
* @param {import('exceljs').Worksheet} worksheet - The worksheet to auto-fit
* @param {number} [minWidth=8] - Minimum column width
* @param {number} [maxWidth=50] - Maximum column width
*/
function autoFitColumns(worksheet, minWidth = 8, maxWidth = 50) {
worksheet.columns.forEach((column, colIndex) => {
let maxLength = minWidth;
worksheet.eachRow({ includeEmpty: false }, (row) => {
const cell = row.getCell(colIndex + 1);
const cellValue = cell.value;
let cellLength = minWidth;
if (cellValue !== null && cellValue !== undefined) {
if (typeof cellValue === "string") {
cellLength = cellValue.length;
} else if (cellValue instanceof Date) {
cellLength = 12;
} else if (typeof cellValue === "number") {
cellLength = cellValue.toString().length;
} else if (typeof cellValue === "object" && cellValue.richText) {
cellLength = cellValue.richText.reduce(
(acc, rt) => acc + (rt.text?.length || 0),
0
);
} else {
cellLength = String(cellValue).length;
}
}
maxLength = Math.max(maxLength, cellLength);
});
column.width = Math.min(maxLength + 2, maxWidth);
});
}
/**
* Applies header styling to the first row of a worksheet.
* @param {import('exceljs').Worksheet} worksheet - The worksheet to style
* @param {Object} [options] - Styling options
* @param {boolean} [options.bold=true] - Make headers bold
* @param {string} [options.fill] - Background color (ARGB format, e.g., 'FF4472C4')
* @param {string} [options.fontColor] - Font color (ARGB format, e.g., 'FFFFFFFF')
*/
function applyHeaderStyle(
worksheet,
{ bold = true, fill = "FF4472C4", fontColor = "FFFFFFFF" } = {}
) {
const headerRow = worksheet.getRow(1);
if (!headerRow || headerRow.cellCount === 0) return;
headerRow.eachCell((cell) => {
cell.font = {
bold,
color: { argb: fontColor },
};
cell.fill = {
type: "pattern",
pattern: "solid",
fgColor: { argb: fill },
};
cell.alignment = {
vertical: "middle",
horizontal: "center",
};
});
headerRow.height = 20;
}
/**
* Applies alternating row colors (zebra striping) to a worksheet.
* @param {import('exceljs').Worksheet} worksheet - The worksheet to style
* @param {string} [evenColor='FFF2F2F2'] - Color for even rows (ARGB format)
* @param {number} [startRow=2] - Row to start alternating from (skips header)
*/
function applyZebraStriping(worksheet, evenColor = "FFF2F2F2", startRow = 2) {
worksheet.eachRow({ includeEmpty: false }, (row, rowNumber) => {
if (rowNumber >= startRow && rowNumber % 2 === 0) {
row.eachCell((cell) => {
if (!cell.fill || cell.fill.type !== "pattern") {
cell.fill = {
type: "pattern",
pattern: "solid",
fgColor: { argb: evenColor },
};
}
});
}
});
}
/**
* Freezes the header row and optionally first columns.
* @param {import('exceljs').Worksheet} worksheet - The worksheet to modify
* @param {number} [rows=1] - Number of rows to freeze
* @param {number} [columns=0] - Number of columns to freeze
*/
function freezePanes(worksheet, rows = 1, columns = 0) {
worksheet.views = [{ state: "frozen", xSplit: columns, ySplit: rows }];
}
module.exports = {
parseCSV,
validateCSVData,
detectDelimiter,
inferCellType,
applyBranding,
autoFitColumns,
applyHeaderStyle,
applyZebraStriping,
freezePanes,
};

View File

@@ -1,6 +1,6 @@
const { FilesystemReadTextFile } = require("./read-text-file.js");
const { FilesystemReadMultipleFiles } = require("./read-multiple-files.js");
const { FilesystemWriteFile } = require("./write-file.js");
const { FilesystemWriteTextFile } = require("./write-text-file.js");
const { FilesystemEditFile } = require("./edit-file.js");
const { FilesystemCreateDirectory } = require("./create-directory.js");
const { FilesystemListDirectory } = require("./list-directory.js");
@@ -17,7 +17,7 @@ const filesystemAgent = {
plugin: [
FilesystemReadTextFile,
FilesystemReadMultipleFiles,
FilesystemWriteFile,
FilesystemWriteTextFile,
FilesystemEditFile,
FilesystemCreateDirectory,
FilesystemListDirectory,

View File

@@ -1,18 +1,19 @@
const filesystem = require("./lib.js");
module.exports.FilesystemWriteFile = {
name: "filesystem-write-file",
module.exports.FilesystemWriteTextFile = {
name: "filesystem-write-text-file",
plugin: function () {
return {
name: "filesystem-write-file",
name: "filesystem-write-text-file",
setup(aibitat) {
aibitat.function({
super: aibitat,
name: this.name,
description:
"Create a new file or completely overwrite an existing file with new content. " +
"Create a new text file or completely overwrite an existing text file with new content. " +
"Use with caution as it will overwrite existing files without warning. " +
"Handles text content with proper encoding. Only works within allowed directories.",
"Only handles text/plaintext content with proper encoding. Only works within allowed directories. " +
"For binary formats (PDF, DOCX, XLSX, PPTX), use the appropriate document creation tools instead.",
examples: [
{
prompt: "Create a new config file with these settings",
@@ -49,7 +50,7 @@ module.exports.FilesystemWriteFile = {
handler: async function ({ path: filePath = "", content = "" }) {
try {
this.super.handlerProps.log(
`Using the filesystem-write-file tool.`
`Using the filesystem-write-text-file tool.`
);
const validPath = await filesystem.validatePath(filePath);
@@ -76,7 +77,7 @@ module.exports.FilesystemWriteFile = {
return `Successfully wrote to ${filePath}`;
} catch (e) {
this.super.handlerProps.log(
`filesystem-write-file error: ${e.message}`
`filesystem-write-text-file error: ${e.message}`
);
this.super.introspect(`Error: ${e.message}`);
return `Error writing file: ${e.message}`;

View File

@@ -2,34 +2,34 @@ const { webBrowsing } = require("./web-browsing.js");
const { webScraping } = require("./web-scraping.js");
const { websocket } = require("./websocket.js");
const { docSummarizer } = require("./summarize.js");
const { saveFileInBrowser } = require("./save-file-browser.js");
const { chatHistory } = require("./chat-history.js");
const { memory } = require("./memory.js");
const { rechart } = require("./rechart.js");
const { sqlAgent } = require("./sql-agent/index.js");
const { filesystemAgent } = require("./filesystem/index.js");
const { createFilesAgent } = require("./create-files/index.js");
module.exports = {
webScraping,
webBrowsing,
websocket,
docSummarizer,
saveFileInBrowser,
chatHistory,
memory,
rechart,
sqlAgent,
filesystemAgent,
createFilesAgent,
// Plugin name aliases so they can be pulled by slug as well.
[webScraping.name]: webScraping,
[webBrowsing.name]: webBrowsing,
[websocket.name]: websocket,
[docSummarizer.name]: docSummarizer,
[saveFileInBrowser.name]: saveFileInBrowser,
[chatHistory.name]: chatHistory,
[memory.name]: memory,
[rechart.name]: rechart,
[sqlAgent.name]: sqlAgent,
[filesystemAgent.name]: filesystemAgent,
[createFilesAgent.name]: createFilesAgent,
};

View File

@@ -1,95 +0,0 @@
const { Deduplicator } = require("../utils/dedupe");
const saveFileInBrowser = {
name: "save-file-to-browser",
startupConfig: {
params: {},
},
plugin: function () {
return {
name: this.name,
setup(aibitat) {
// List and summarize the contents of files that are embedded in the workspace
aibitat.function({
super: aibitat,
tracker: new Deduplicator(),
name: this.name,
description:
"Download or export content as a file. Save text, code, data, or conversation content to a downloadable file. Use when the user wants to save, download, or export something as a file.",
examples: [
{
prompt: "Download that as a file",
call: JSON.stringify({
file_content: "<content to save>",
filename: "download.txt",
}),
},
{
prompt: "Save that code to a file",
call: JSON.stringify({
file_content: "<code content>",
filename: "code.js",
}),
},
{
prompt: "Save me that to a file named 'output'",
call: JSON.stringify({
file_content: "<content of the file>",
filename: "output.txt",
}),
},
],
parameters: {
$schema: "http://json-schema.org/draft-07/schema#",
type: "object",
properties: {
file_content: {
type: "string",
description: "The content of the file that will be saved.",
},
filename: {
type: "string",
description:
"filename to save the file as with extension. Extension should be plaintext file extension.",
},
},
additionalProperties: false,
},
handler: async function ({ file_content = "", filename }) {
try {
const { isDuplicate, reason } = this.tracker.isDuplicate(
this.name,
{ file_content, filename }
);
if (isDuplicate) {
this.super.handlerProps.log(
`${this.name} was called, but exited early because ${reason}.`
);
return `${filename} file has been saved successfully!`;
}
this.super.socket.send("fileDownload", {
filename,
b64Content:
"data:text/plain;base64," +
Buffer.from(file_content, "utf8").toString("base64"),
});
this.super.introspect(`${this.caller}: Saving file ${filename}.`);
this.tracker.trackRun(this.name, { file_content, filename });
return `${filename} file has been saved successfully and will be downloaded automatically to the users browser.`;
} catch (error) {
this.super.handlerProps.log(
`save-file-to-browser raised an error. ${error.message}`
);
return `Let the user know this action was not successful. An error was raised while saving a file to the browser. ${error.message}`;
}
},
});
},
};
},
};
module.exports = {
saveFileInBrowser,
};

View File

@@ -81,6 +81,15 @@ async function agentSkillsFromSystemSettings() {
[]
);
// Load disabled create-files sub-skills
const _disabledCreateFilesSkills = safeJsonParse(
await SystemSettings.getValueOrFallback(
{ label: "disabled_create_files_skills" },
"[]"
),
[]
);
// Load non-imported built-in skills that are configurable.
const _setting = safeJsonParse(
await SystemSettings.getValueOrFallback(
@@ -106,6 +115,16 @@ async function agentSkillsFromSystemSettings() {
if (_disabledFilesystemSkills.includes(subPlugin.name)) continue;
}
/**
* If the create-files tool is not available, or the sub-skill is explicitly disabled, skip it
* This is a docker specific skill so it cannot be used in other environments.
*/
if (skillName === "create-files-agent") {
const createFilesTool = require("./aibitat/plugins/create-files/lib");
if (!createFilesTool.isToolAvailable()) continue;
if (_disabledCreateFilesSkills.includes(subPlugin.name)) continue;
}
systemFunctions.push(
`${AgentPlugins[skillName].name}#${subPlugin.name}`
);

View File

@@ -157,6 +157,7 @@ function convertToChatHistory(history = []) {
sentAt: moment(createdAt).unix(),
feedbackScore,
metrics: data?.metrics || {},
...(data?.outputs?.length > 0 ? { outputs: data.outputs } : {}),
},
]);
}

View File

@@ -42,7 +42,7 @@ async function handleAgentResponse(
const sources = [];
const thoughts = [];
const charts = [];
const files = [];
const _files = [];
let thoughtMsgId = null;
let lastThoughtText = "";
@@ -130,8 +130,8 @@ async function handleAgentResponse(
case "rechartVisualize":
if (parsed.content) charts.push(parsed.content);
return;
case "fileDownload":
if (parsed.content) files.push(parsed.content);
case "fileDownloadCard":
if (parsed.content) _files.push(parsed.content);
return;
case "reportStreamEvent":
const inner = parsed.content;
@@ -170,8 +170,16 @@ async function handleAgentResponse(
? "✓ <b>Agent completed:</b>"
: "🤔 <b>Agent is thinking:</b>";
const icon = done ? "✓" : "⏳";
const maxThoughtLen = 100;
const content = thoughtList
.map((t) => `${icon} ${escapeHTML(t)}`)
.map((t) => {
const escaped = escapeHTML(t);
const truncated =
escaped.length > maxThoughtLen
? escaped.slice(0, maxThoughtLen) + "..."
: escaped;
return `${icon} ${truncated}`;
})
.join("\n");
const fullContent = `${header}\n${content}`;
const tag =
@@ -223,8 +231,9 @@ async function handleAgentResponse(
ctx.bot.sendChatAction(chatId, "typing").catch(() => {});
}, 4000);
let agentHandler = null;
try {
const agentHandler = await new EphemeralAgentHandler({
agentHandler = await new EphemeralAgentHandler({
uuid: uuidv4(),
workspace,
prompt: message,
@@ -239,11 +248,9 @@ async function handleAgentResponse(
agentHandler.aibitat.maxRounds = 2;
await agentHandler.startAgentCluster();
} finally {
clearInterval(typingInterval);
clearTimeout(thoughtFlushTimeout);
clearTimeout(editTimer);
}
// Extract pending outputs from aibitat for persistence
const outputs = agentHandler?.aibitat?._pendingOutputs ?? [];
// Final thought update, mark as completed
if (thoughtMsgId && thoughts.length > 0) {
@@ -276,23 +283,6 @@ async function handleAgentResponse(
}
}
// Send files as Telegram documents
for (const file of files) {
try {
const base64Data = file.b64Content.split(",")[1];
const buffer = Buffer.from(base64Data, "base64");
await ctx.bot.sendDocument(
chatId,
buffer,
{},
{
filename: file.filename,
contentType: "application/octet-stream",
}
);
} catch {}
}
// Ensure the initial sendMessage has resolved before deciding how to deliver
if (responsePending) await responsePending;
@@ -310,6 +300,7 @@ async function handleAgentResponse(
type: "chat",
metrics,
attachments,
...(outputs.length > 0 ? { outputs } : {}),
},
threadId: thread?.id || null,
});
@@ -336,6 +327,63 @@ async function handleAgentResponse(
await sendVoiceResponse(ctx.bot, chatId, responseText);
}
}
// Send files as Telegram documents (after response is delivered)
if (_files.length > 0) {
ctx.log?.info?.(`Sending ${_files.length} file(s) to Telegram`);
await sendFilesAsTelegramDocuments(ctx, chatId, _files);
}
} finally {
clearInterval(typingInterval);
clearTimeout(thoughtFlushTimeout);
clearTimeout(editTimer);
}
}
/**
* Send generated files as Telegram documents without blocking the main response.
* @param {import("../commands").BotContext} ctx
* @param {number} chatId
* @param {Array<{filename: string, storageFilename: string, fileSize: number}>} files
*/
async function sendFilesAsTelegramDocuments(ctx, chatId, files) {
const createFilesLib = require("../../agents/aibitat/plugins/create-files/lib");
for (const file of files) {
try {
ctx.log?.info?.(`Retrieving file: ${file.storageFilename}`);
const result = await createFilesLib.getGeneratedFile(
file.storageFilename
);
if (!result?.buffer) {
ctx.log?.warn?.(
`Could not retrieve generated file: ${file.storageFilename}`
);
continue;
}
const extension = file.storageFilename.split(".").pop() || "";
const mimeType = createFilesLib.getMimeType(extension);
ctx.log?.info?.(
`Sending document: ${file.filename} (${result.buffer.length} bytes, ${mimeType})`
);
await ctx.bot.sendDocument(
chatId,
result.buffer,
{ caption: file.filename },
{
filename: file.filename,
contentType: mimeType,
}
);
ctx.log?.info?.(`Successfully sent document: ${file.filename}`);
} catch (err) {
ctx.log?.error?.(
`Failed to send document ${file.filename}:`,
err.message
);
}
}
}
/**

File diff suppressed because it is too large Load Diff