// storage-adapter-import-placeholder import { postgresAdapter } from "@payloadcms/db-postgres"; import { HeadingFeature, lexicalEditor } from "@payloadcms/richtext-lexical"; import path from "path"; import { buildConfig } from "payload"; import { fileURLToPath } from "url"; import sharp from "sharp"; import { Users } from "@/collections/users/Users"; import { Roles } from "@/collections/users/Roles"; import { Awards } from "@/collections/users/Awards"; import { Media } from "@/collections/Media"; import { Ranks } from "@/collections/users/Ranks"; import { Profiles } from "@/collections/users/Profiles"; import { Missions } from "@/collections/intelligence/Missions"; import { MissionAttendances } from "@/collections/intelligence/MissionAttendances"; import { Maps } from "@/collections/world/Maps"; import { Campaigns } from "@/collections/intelligence/Campaigns"; import { Qualifications } from "@/collections/users/Qualifications"; import { MissionFiles } from "@/collections/server/MissionFiles"; import { ModLists } from "@/collections/server/ModLists"; import { Factions } from "@/collections/intelligence/Factions"; import { Assets } from "@/collections/logistics/Assets"; import { NarrativeEvents } from "@/collections/world/NarrativeEvents"; import { Resources } from "@/collections/logistics/Resources"; import { Vehicles } from "@/collections/logistics/Vehicles"; import { Structures } from "@/collections/logistics/Structures"; import { Shipments } from "@/collections/logistics/Shipments"; import { Assignments } from "@/collections/users/Assignments"; import { Technologies } from "@/collections/intelligence/Technologies"; import { GameRules } from "@/collections/game/GameRules"; import { GameStructures } from "@/collections/game/GameStructures"; import { GameHardResources } from "@/collections/game/GameHardResources"; import { GameEventLogs } from "@/collections/game/GameEventLogs"; import { GameVehicles } from "@/collections/game/GameVehicles"; import { GameNpcs } from "@/collections/game/GameNpcs"; import { Experience } from "@/collections/users/Experience"; import { BankAccounts } from "@/collections/banking/BankAccounts"; import { BankTransactions } from "@/collections/banking/BankTransactions"; import { LedgerEntries } from "@/collections/banking/LedgerEntries"; import { LockerStorages } from "@/collections/locker/LockerStorages"; import { Loadouts } from "@/collections/locker/Loadouts"; import { MarketListings } from "@/collections/market/MarketListings"; import { MarketNegotiations } from "@/collections/market/MarketNegotiations"; import { UserNotifications } from "@/collections/users/UserNotifications"; import { Tickets } from "@/collections/tickets/Tickets"; import { Projects } from "@/collections/projects/Projects"; import { Sprints } from "@/collections/projects/Sprints"; import { Releases } from "@/collections/projects/Releases"; import { Labels } from "@/collections/projects/Labels"; import { migrations } from "./migrations"; import { nodemailerAdapter } from "@payloadcms/email-nodemailer"; import nodemailer from "nodemailer"; import { Shims } from "@/collections/Shims"; import { payloadAiPlugin, PayloadAiPluginLexicalEditorFeature } from "@ai-stack/payloadcms"; import { createOpenAI } from "@ai-sdk/openai"; import { createTextStreamResponse, jsonSchema, streamObject } from "ai"; const unslothProvider = createOpenAI({ apiKey: process.env.UNSLOTH_API_KEY, baseURL: process.env.UNSLOTH_URL || "https://uns.onyxsimple.com/v1", }); const DEFAULT_MODEL = "unsloth/gemma-4-E4B-it-qat-GGUF"; function plainTextToLexicalJson(text: string): Record { const paragraphs = text.split(/\n\n+/).filter((p) => p.trim()); const children = paragraphs.map((para) => ({ type: "paragraph", children: [ { type: "text", text: para.trim(), format: 0, style: "", mode: "normal", detail: 0, direction: "ltr", indent: 0, version: 1, }, ], direction: "ltr", format: "start", indent: 0, textFormat: 0, textStyle: "", version: 1, })); return { root: { type: "root", children, direction: "ltr", format: "", indent: 0, version: 1, }, }; } async function generatePlainText( prompt: string, system: string, model: string, ): Promise { const baseUrl = process.env.UNSLOTH_URL || "https://uns.onyxsimple.com/v1"; const response = await fetch(`${baseUrl}/chat/completions`, { method: "POST", headers: { Authorization: "Bearer " + process.env.UNSLOTH_API_KEY, "Content-Type": "application/json", }, body: JSON.stringify({ model, messages: [ { role: "system", content: system || "You are a helpful assistant." }, { role: "user", content: prompt }, ], stream: false, }), }); if (!response.ok) { throw new Error(`Unsloth API error: ${response.statusText}`); } const result = await response.json(); return result.choices[0].message.content; } const filename = fileURLToPath(import.meta.url); const dirname = path.dirname(filename); const META_TITLE = "Polaris Task Force"; const META_DESCRIPTION = "A gamified platform for Polaris Task Force, an Arma 3 unit."; export default buildConfig({ // serverURL: "http://localhost:3000/", admin: { user: Users.slug, importMap: { baseDir: path.resolve(dirname), }, meta: { titleSuffix: "- Polaris Task Force", applicationName: META_TITLE, abstract: META_DESCRIPTION, keywords: "Polaris Task Force, Polaris, Task Force, Polaris Task Force official website", description: META_DESCRIPTION, openGraph: { title: META_TITLE, description: META_DESCRIPTION, url: "https://polaris-task-force.com", siteName: META_TITLE, images: [ { url: "/images/icon-favicon.png", width: 512, height: 512, alt: "Polaris Task Force Logo", }, ], }, icons: [ { rel: "icon", type: "image/png", url: "/images/icon-favicon.png", }, ], }, components: { beforeDashboard: ["@/components/static/BeforeDashboardText"], header: ["@/components/static/VersionOverlay"], beforeNav: ["@/components/static/GoToFrontendLink"], graphics: { Logo: "@/components/graphics/AppLogo", Icon: "@/components/graphics/AppIcon", }, }, }, globals: [GameRules, Shims], collections: [ Media, // Game GameHardResources, GameStructures, GameVehicles, GameNpcs, GameEventLogs, // Users Users, Roles, Ranks, Profiles, Awards, Qualifications, Assignments, Experience, UserNotifications, // Intel Missions, MissionAttendances, Campaigns, Factions, Technologies, // Logistics Assets, Resources, Vehicles, Structures, Shipments, // Banking BankAccounts, BankTransactions, LedgerEntries, // Locker LockerStorages, Loadouts, // Market MarketListings, MarketNegotiations, // Helpdesk Tickets, // Projects Projects, Sprints, Releases, Labels, // World Maps, NarrativeEvents, // Server MissionFiles, ModLists, ], editor: lexicalEditor({ features: ({ rootFeatures }) => { return [HeadingFeature(), PayloadAiPluginLexicalEditorFeature()]; }, }), secret: process.env.PAYLOAD_SECRET || "", typescript: { outputFile: path.resolve(dirname, "payload-types.ts"), }, db: postgresAdapter({ pool: { connectionString: process.env.DATABASE_URI || "", }, push: process.env.NODE_ENV === "development", prodMigrations: migrations, }), sharp, bin: [ { scriptPath: path.resolve(dirname, "scripts/gameTick.ts"), key: "game-tick", }, { scriptPath: path.resolve(dirname, "scripts/marketTick.ts"), key: "market-tick", }, { scriptPath: path.resolve(dirname, "scripts/generateNextMission.ts"), key: "generate-mission", }, ], plugins: [ // payloadCloudPlugin(), // storage-adapter-placeholder payloadAiPlugin({ collections: { [GameHardResources.slug]: true, [GameStructures.slug]: true, [GameVehicles.slug]: true, [GameNpcs.slug]: true, [GameEventLogs.slug]: true, // Users [Users.slug]: true, [Roles.slug]: true, [Ranks.slug]: true, [Profiles.slug]: true, [Awards.slug]: true, [Qualifications.slug]: true, [Assignments.slug]: true, [Experience.slug]: true, [UserNotifications.slug]: true, // Intel [Missions.slug]: true, [MissionAttendances.slug]: true, [Campaigns.slug]: true, [Factions.slug]: true, [Technologies.slug]: true, // Logistics [Assets.slug]: true, [Resources.slug]: true, [Vehicles.slug]: true, [Structures.slug]: true, [Shipments.slug]: true, // Banking [BankAccounts.slug]: true, [BankTransactions.slug]: true, [LedgerEntries.slug]: true, // Locker [LockerStorages.slug]: true, [Loadouts.slug]: true, // Market [MarketListings.slug]: true, [MarketNegotiations.slug]: true, // Helpdesk [Tickets.slug]: true, // Projects [Projects.slug]: true, [Sprints.slug]: true, [Releases.slug]: true, [Labels.slug]: true, }, debugging: true, generationModels: [ { id: "unsloth-text", name: "Unsloth", fields: ["text", "textarea", "richText"], handler: async ( prompt: string, options: { locale: string; model: string; system: string; schema: Record; maxTokens?: number; temperature?: number; }, ) => { const model = options.model || DEFAULT_MODEL; if (options.schema?.definitions) { const text = await generatePlainText(prompt, options.system, model); const lexicalJson = plainTextToLexicalJson(text); const ts = new ReadableStream({ start(controller) { controller.enqueue(JSON.stringify(lexicalJson)); controller.close(); }, }); return createTextStreamResponse({ textStream: ts }); } const streamResult = streamObject({ maxOutputTokens: options.maxTokens || 5000, model: unslothProvider(model), onError: (error) => { console.error("Unsloth generateObject: ", error); }, prompt, schema: jsonSchema(options.schema), system: options.system, temperature: options.temperature ?? 0.7, providerOptions: { openai: { strictJsonSchema: true, structuredOutputs: true, }, }, }); const textStream = new ReadableStream({ async start(controller) { try { for await (const chunk of streamResult.textStream) { controller.enqueue(chunk); } if ((await streamResult.finishReason) === "length") { controller.enqueue("\u0000"); } controller.close(); } catch (error) { controller.error(error); } }, }); return createTextStreamResponse({ textStream }); }, generateText: async (prompt: string, system: string) => { return generatePlainText(prompt, system, DEFAULT_MODEL); }, output: "text", settings: { name: "unsloth-text-settings", type: "group", admin: { condition(data) { return data["model-id"] === "unsloth-text"; }, }, fields: [ { name: "model", type: "text", defaultValue: DEFAULT_MODEL, label: "Model", }, ], label: "Unsloth Settings", }, }, ], }), ], email: nodemailerAdapter({ defaultFromAddress: process.env.EMAIL_FROM_ADDRESS || "", defaultFromName: process.env.EMAIL_FROM_NAME || "", transport: nodemailer.createTransport({ host: process.env.EMAIL_HOST || "", port: Number(process.env.EMAIL_PORT) || 587, auth: { user: process.env.EMAIL_USERNAME || "", pass: process.env.EMAIL_PASSWORD || "", }, }), }), });