Introduce writing styles

This commit is contained in:
2025-06-06 16:04:38 +03:00
parent 4d889fe114
commit 6911cfaaa4
9 changed files with 94 additions and 51 deletions

11
package-lock.json generated
View File

@@ -23,7 +23,8 @@
"radash": "^12.1.0",
"url": "^0.11.4",
"vue": "^3.5.13",
"vue-router": "^4.5.0"
"vue-router": "^4.5.0",
"zod": "^3.25.55"
},
"devDependencies": {
"drizzle-kit": "^0.30.6",
@@ -13202,12 +13203,10 @@
}
},
"node_modules/zod": {
"version": "3.24.2",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.24.2.tgz",
"integrity": "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==",
"version": "3.25.55",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.55.tgz",
"integrity": "sha512-219huNnkSLQnLsQ3uaRjXsxMrVm5C9W3OOpEVt2k5tvMKuA8nBSu38e0B//a+he9Iq2dvmk2VyYVlHqiHa4YBA==",
"license": "MIT",
"optional": true,
"peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}

View File

@@ -25,7 +25,8 @@
"radash": "^12.1.0",
"url": "^0.11.4",
"vue": "^3.5.13",
"vue-router": "^4.5.0"
"vue-router": "^4.5.0",
"zod": "^3.25.55"
},
"devDependencies": {
"drizzle-kit": "^0.30.6",

View File

@@ -1,5 +1,4 @@
<script setup lang="ts">
import { diff, omit } from "radash";
import { trackEvent } from "@aptabase/web";
useHead({ title: "Ghostwriter" });
@@ -14,24 +13,23 @@ interface FormData {
enabled: boolean;
language: string;
units: string;
tone: string[];
}
const preferences = useState<FormData>("preferences", () => ({
enabled: false,
language: "English",
units: "Metric",
tone: [],
}));
const { status } = useFetch("/api/preferences", {
onResponse({ error, response }) {
if (error) {
return;
}
preferences.value = omit(response._data, ["tone"]) as FormData;
},
const { data, status } = useFetch("/api/preferences", {
lazy: true,
});
// @ts-expect-error typing issue - missing transform
syncRef(data, preferences, { direction: "ltr", deep: true });
onMounted(() => {
saveOp.resume();
});
@@ -40,7 +38,7 @@ const saveOp = watchPausable(
preferences,
async (newData) => {
trackEvent("update_preferences", {
...newData,
enabled: newData.enabled,
});
await $fetch("/api/preferences", {
@@ -75,7 +73,9 @@ const saveOp = watchPausable(
<div class="font-bold text-lg"> Support</div>
<UCard class="">
<div class="flex flex-col gap-8">
Ghostwriter 👻 is free to use, but it takes time and resources to keep it running smoothly. If you enjoy it, consider supporting the app and its creator - every bit helps!
Ghostwriter 👻 is free to use, but it takes time and resources to keep
it running smoothly. If you enjoy it, consider supporting the app and
its creator - every bit helps!
</div>
<template #footer>
<ULink href="https://buymeacoffee.com/mariosant" target="_blank">
@@ -133,6 +133,22 @@ const saveOp = watchPausable(
/>
</template>
</CardField>
<CardField>
<template #title> Tone </template>
<template #description>
Choose one or more writing styles you like.
</template>
<template #value>
<USelect
multiple
class="min-w-28 max-w-64"
v-model="preferences.tone"
:items="tones"
placeholder="None specified (Use all)"
/>
</template>
</CardField>
</div>
</UCard>
</UContainer>

View File

@@ -1,7 +1,22 @@
import * as z from "zod";
import {
availableLanguages,
availableTones,
availableUnits,
} from "~/shared/constants";
//
const bodySchema = z.strictObject({
enabled: z.boolean(),
language: z.enum(availableLanguages),
units: z.enum(availableUnits),
tone: z.array(z.enum(availableTones)),
});
export default defineEventHandler(async (event) => {
const session = await requireUserSession(event);
const body = await readValidatedBody(event, (body) => bodySchema.parse(body));
const db = useDrizzle();
const body = await readBody(event);
const [preferences] = await db
.update(tables.preferences)
@@ -10,6 +25,7 @@ export default defineEventHandler(async (event) => {
enabled: body.enabled,
language: body.language,
units: body.units,
tone: body.tone,
},
})
.where(eq(tables.preferences.userId, session.user.id))

View File

@@ -33,11 +33,13 @@ export const preferences = pgTable("preferences", {
enabled: boolean;
language: string;
units: "Imperial" | "Metric";
tone?: string[];
}>()
.$defaultFn(() => ({
enabled: true,
language: "English",
units: "Metric",
tone: [],
})),
});

View File

@@ -1,6 +1,7 @@
import { chain, draw, get, omit, pick, tryit } from "radash";
import { chain, draw, get, isEmpty, omit, pick, tryit } from "radash";
import { safeDestr } from "destr";
import { User } from "./drizzle";
import { availableTones } from "~/shared/constants";
const promo = "Written by https://ghostwriter.rocks 👻";
@@ -111,18 +112,13 @@ export const createActivityContent = async ({
}: {
currentActivity: Activity;
previousActivities: Activity[];
user: User & { preferences: any };
user: User & { preferences: Preferences };
}) => {
const openai = useOpenAI();
const tone = draw([
"casual",
"funny",
"epic",
"poetic",
"reflective",
"snarky",
]);
const tone = isEmpty(user.preferences.data?.tone)
? (draw(availableTones) as string)
: draw(user.preferences.data!.tone!);
const length = draw([
"short",
@@ -135,7 +131,7 @@ export const createActivityContent = async ({
const prompt = `
Generate a short title and a ${length}-lengthed description for my strava activity. Use my preferred language and unit system.
Try to not exaggerate as I am using Strava often and I want my activites to be unique and easy to read. Don't say things like nothing too fancy or wild.
Use a little bit of ${tone} to make things less boring. Highlight any PR only if available, do not mention them if no PRs.
Use a little bit of ${tone} tone to make things less boring. Highlight any PR only if available, do not mention them if no PRs.
Maybe comment if any interesting fact in comparison to previous activities.
Add #${tone} at the end of the description. Depending the length of the description, maybe add more hashtags.

View File

@@ -12,4 +12,5 @@ export function useDrizzle() {
}
export type User = typeof schema.users.$inferSelect;
export type Preferences = typeof schema.preferences.$inferSelect;
export type Tokens = typeof schema.tokens.$inferSelect;

24
shared/constants.ts Normal file
View File

@@ -0,0 +1,24 @@
export const availableTones = [
"Adventure",
"Casual",
"Competitive",
"Epic",
"Funny",
"Minimalist",
"Motivational",
"Poetic",
"Reflective",
"Snarky",
"Witty",
] as const;
export const availableLanguages = [
"English",
"Greek",
"German",
"Italian",
"Polish",
"Spanish",
] as const;
export const availableUnits = ["Imperial", "Metric"] as const;

View File

@@ -1,23 +1,11 @@
export const languages = ref([
"English",
"Greek",
"German",
"Italian",
"Polish",
"Spanish",
]);
import {
availableLanguages,
availableTones,
availableUnits,
} from "~/shared/constants";
export const tones = ref([
"Motivational",
"Casual",
"Funny",
"Epic",
"Minimalist",
"Reflective",
"Poetic",
"Competitive",
"Adventure",
"Snarky",
]);
export const languages = ref(availableLanguages);
export const units = ref(["Imperial", "Metric"]);
export const tones = ref(availableTones);
export const units = ref(availableUnits);