Files
ghostwriter/server/utils/create-content.ts

163 lines
3.7 KiB
TypeScript

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<string, any>;
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;
};