import { chain, draw, get, isEmpty, omit, tryit } from "radash"; import { safeDestr } from "destr"; import { match } from "ts-pattern"; import { User } from "./drizzle"; import { availableHighlights, availableTones } from "~/shared/constants"; const promo = "Written by https://ghostwriter.rocks 👻"; type Activity = Record; const movingActivityTypes = [ "AlpineSki", "BackcountrySki", "Canoeing", "Crossfit", "EBikeRide", "Elliptical", "Handcycle", "Hike", "IceSkate", "InlineSkate", "Kayaking", "Kitesurf", "NordicSki", "Ride", "RockClimbing", "RollerSki", "Rowing", "Run", "Sail", "Skateboard", "Snowboard", "Snowshoe", "StandUpPaddling", "Surfing", "Swim", "Velomobile", "VirtualRide", "VirtualRun", "Walk", "Wheelchair", "Windsurf", "Yoga", ]; const staticActivityTypes = [ "Soccer", "Workout", "WeightTraining", "StairStepper", "Golf", ]; const stringifyActivity = chain( ({ activity, shouldKeepNames = false }) => { const baseFieldsToOmit = [ "laps", "splits_metric", "splits_standard", "hide_from_home", "available_zones", "map", "start_date_local", "gear", "stats_visibility", "embed_token", ]; const nameFields = shouldKeepNames ? [] : ["name", "description"]; if (movingActivityTypes.includes(activity.type)) { return omit(activity, [...baseFieldsToOmit, ...nameFields]); } if (staticActivityTypes.includes(activity.type)) { return omit(activity, [...baseFieldsToOmit, ...nameFields, "distance"]); } return omit(activity, [...baseFieldsToOmit, ...nameFields]); }, (activity) => JSON.stringify(activity), ); export const createActivityContent = async ({ currentActivity, previousActivities, user, }: { currentActivity: Activity; previousActivities: Activity[]; user: User & { preferences: Preferences }; }) => { const openai = useOpenAI(); const tone = isEmpty(user.preferences.data?.tone) ? (draw(availableTones) as string) : draw(user.preferences.data!.tone!); const highlight = isEmpty(user.preferences.data?.highlights) ? (draw(availableHighlights) as string) : draw(user.preferences.data!.highlights!); const length = match({ tone }) .with({ tone: "Minimalist" }, () => "very short") .otherwise(() => draw(["very short", "short"])); const prompt = ` Language: ${user?.preferences.data!.language} Unit system: ${user?.preferences.data!.units} Tone: ${tone} Highlight: ${highlight} Description length: ${length} The activity data in json format from strava: ${stringifyActivity({ activity: currentActivity })} The recent previous activities in json format: [${previousActivities.map((activity) => stringifyActivity({ activity, shouldKeepNames: true }))}] `; const aiResponse = await openai.responses.create({ model: "@preset/ghostwriter", input: [{ role: "user", content: prompt }], reasoning: { effort: "minimal", }, text: { format: { type: "json_schema", name: "activity", schema: { type: "object", properties: { title: { type: "string", }, description: { type: "string", }, }, required: ["title", "description"], additionalProperties: false, }, }, }, }); const [parseError, responseObject] = tryit( chain( (r) => get(r, "output_text"), (r) => safeDestr<{ title: string; description: string }>(r), ), )(aiResponse); const stravaRequestBody = { name: responseObject!.title, description: responseObject!.description, }; return [parseError, stravaRequestBody] as const; };