Introduce writing styles
This commit is contained in:
11
package-lock.json
generated
11
package-lock.json
generated
@@ -23,7 +23,8 @@
|
|||||||
"radash": "^12.1.0",
|
"radash": "^12.1.0",
|
||||||
"url": "^0.11.4",
|
"url": "^0.11.4",
|
||||||
"vue": "^3.5.13",
|
"vue": "^3.5.13",
|
||||||
"vue-router": "^4.5.0"
|
"vue-router": "^4.5.0",
|
||||||
|
"zod": "^3.25.55"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"drizzle-kit": "^0.30.6",
|
"drizzle-kit": "^0.30.6",
|
||||||
@@ -13202,12 +13203,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/zod": {
|
"node_modules/zod": {
|
||||||
"version": "3.24.2",
|
"version": "3.25.55",
|
||||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.24.2.tgz",
|
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.55.tgz",
|
||||||
"integrity": "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==",
|
"integrity": "sha512-219huNnkSLQnLsQ3uaRjXsxMrVm5C9W3OOpEVt2k5tvMKuA8nBSu38e0B//a+he9Iq2dvmk2VyYVlHqiHa4YBA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
|
||||||
"peer": true,
|
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/colinhacks"
|
"url": "https://github.com/sponsors/colinhacks"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,8 @@
|
|||||||
"radash": "^12.1.0",
|
"radash": "^12.1.0",
|
||||||
"url": "^0.11.4",
|
"url": "^0.11.4",
|
||||||
"vue": "^3.5.13",
|
"vue": "^3.5.13",
|
||||||
"vue-router": "^4.5.0"
|
"vue-router": "^4.5.0",
|
||||||
|
"zod": "^3.25.55"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"drizzle-kit": "^0.30.6",
|
"drizzle-kit": "^0.30.6",
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { diff, omit } from "radash";
|
|
||||||
import { trackEvent } from "@aptabase/web";
|
import { trackEvent } from "@aptabase/web";
|
||||||
|
|
||||||
useHead({ title: "Ghostwriter" });
|
useHead({ title: "Ghostwriter" });
|
||||||
@@ -14,24 +13,23 @@ interface FormData {
|
|||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
language: string;
|
language: string;
|
||||||
units: string;
|
units: string;
|
||||||
|
tone: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const preferences = useState<FormData>("preferences", () => ({
|
const preferences = useState<FormData>("preferences", () => ({
|
||||||
enabled: false,
|
enabled: false,
|
||||||
language: "English",
|
language: "English",
|
||||||
units: "Metric",
|
units: "Metric",
|
||||||
|
tone: [],
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const { status } = useFetch("/api/preferences", {
|
const { data, status } = useFetch("/api/preferences", {
|
||||||
onResponse({ error, response }) {
|
lazy: true,
|
||||||
if (error) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
preferences.value = omit(response._data, ["tone"]) as FormData;
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// @ts-expect-error typing issue - missing transform
|
||||||
|
syncRef(data, preferences, { direction: "ltr", deep: true });
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
saveOp.resume();
|
saveOp.resume();
|
||||||
});
|
});
|
||||||
@@ -40,7 +38,7 @@ const saveOp = watchPausable(
|
|||||||
preferences,
|
preferences,
|
||||||
async (newData) => {
|
async (newData) => {
|
||||||
trackEvent("update_preferences", {
|
trackEvent("update_preferences", {
|
||||||
...newData,
|
enabled: newData.enabled,
|
||||||
});
|
});
|
||||||
|
|
||||||
await $fetch("/api/preferences", {
|
await $fetch("/api/preferences", {
|
||||||
@@ -75,7 +73,9 @@ const saveOp = watchPausable(
|
|||||||
<div class="font-bold text-lg">❤️ Support</div>
|
<div class="font-bold text-lg">❤️ Support</div>
|
||||||
<UCard class="">
|
<UCard class="">
|
||||||
<div class="flex flex-col gap-8">
|
<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>
|
</div>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<ULink href="https://buymeacoffee.com/mariosant" target="_blank">
|
<ULink href="https://buymeacoffee.com/mariosant" target="_blank">
|
||||||
@@ -133,6 +133,22 @@ const saveOp = watchPausable(
|
|||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</CardField>
|
</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>
|
</div>
|
||||||
</UCard>
|
</UCard>
|
||||||
</UContainer>
|
</UContainer>
|
||||||
|
|||||||
@@ -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) => {
|
export default defineEventHandler(async (event) => {
|
||||||
const session = await requireUserSession(event);
|
const session = await requireUserSession(event);
|
||||||
|
const body = await readValidatedBody(event, (body) => bodySchema.parse(body));
|
||||||
|
|
||||||
const db = useDrizzle();
|
const db = useDrizzle();
|
||||||
const body = await readBody(event);
|
|
||||||
|
|
||||||
const [preferences] = await db
|
const [preferences] = await db
|
||||||
.update(tables.preferences)
|
.update(tables.preferences)
|
||||||
@@ -10,6 +25,7 @@ export default defineEventHandler(async (event) => {
|
|||||||
enabled: body.enabled,
|
enabled: body.enabled,
|
||||||
language: body.language,
|
language: body.language,
|
||||||
units: body.units,
|
units: body.units,
|
||||||
|
tone: body.tone,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.where(eq(tables.preferences.userId, session.user.id))
|
.where(eq(tables.preferences.userId, session.user.id))
|
||||||
|
|||||||
@@ -33,11 +33,13 @@ export const preferences = pgTable("preferences", {
|
|||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
language: string;
|
language: string;
|
||||||
units: "Imperial" | "Metric";
|
units: "Imperial" | "Metric";
|
||||||
|
tone?: string[];
|
||||||
}>()
|
}>()
|
||||||
.$defaultFn(() => ({
|
.$defaultFn(() => ({
|
||||||
enabled: true,
|
enabled: true,
|
||||||
language: "English",
|
language: "English",
|
||||||
units: "Metric",
|
units: "Metric",
|
||||||
|
tone: [],
|
||||||
})),
|
})),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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 { safeDestr } from "destr";
|
||||||
import { User } from "./drizzle";
|
import { User } from "./drizzle";
|
||||||
|
import { availableTones } from "~/shared/constants";
|
||||||
|
|
||||||
const promo = "Written by https://ghostwriter.rocks 👻";
|
const promo = "Written by https://ghostwriter.rocks 👻";
|
||||||
|
|
||||||
@@ -111,18 +112,13 @@ export const createActivityContent = async ({
|
|||||||
}: {
|
}: {
|
||||||
currentActivity: Activity;
|
currentActivity: Activity;
|
||||||
previousActivities: Activity[];
|
previousActivities: Activity[];
|
||||||
user: User & { preferences: any };
|
user: User & { preferences: Preferences };
|
||||||
}) => {
|
}) => {
|
||||||
const openai = useOpenAI();
|
const openai = useOpenAI();
|
||||||
|
|
||||||
const tone = draw([
|
const tone = isEmpty(user.preferences.data?.tone)
|
||||||
"casual",
|
? (draw(availableTones) as string)
|
||||||
"funny",
|
: draw(user.preferences.data!.tone!);
|
||||||
"epic",
|
|
||||||
"poetic",
|
|
||||||
"reflective",
|
|
||||||
"snarky",
|
|
||||||
]);
|
|
||||||
|
|
||||||
const length = draw([
|
const length = draw([
|
||||||
"short",
|
"short",
|
||||||
@@ -135,7 +131,7 @@ export const createActivityContent = async ({
|
|||||||
const prompt = `
|
const prompt = `
|
||||||
Generate a short title and a ${length}-lengthed description for my strava activity. Use my preferred language and unit system.
|
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.
|
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.
|
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.
|
Add #${tone} at the end of the description. Depending the length of the description, maybe add more hashtags.
|
||||||
|
|||||||
@@ -12,4 +12,5 @@ export function useDrizzle() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type User = typeof schema.users.$inferSelect;
|
export type User = typeof schema.users.$inferSelect;
|
||||||
|
export type Preferences = typeof schema.preferences.$inferSelect;
|
||||||
export type Tokens = typeof schema.tokens.$inferSelect;
|
export type Tokens = typeof schema.tokens.$inferSelect;
|
||||||
|
|||||||
24
shared/constants.ts
Normal file
24
shared/constants.ts
Normal 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;
|
||||||
@@ -1,23 +1,11 @@
|
|||||||
export const languages = ref([
|
import {
|
||||||
"English",
|
availableLanguages,
|
||||||
"Greek",
|
availableTones,
|
||||||
"German",
|
availableUnits,
|
||||||
"Italian",
|
} from "~/shared/constants";
|
||||||
"Polish",
|
|
||||||
"Spanish",
|
|
||||||
]);
|
|
||||||
|
|
||||||
export const tones = ref([
|
export const languages = ref(availableLanguages);
|
||||||
"Motivational",
|
|
||||||
"Casual",
|
|
||||||
"Funny",
|
|
||||||
"Epic",
|
|
||||||
"Minimalist",
|
|
||||||
"Reflective",
|
|
||||||
"Poetic",
|
|
||||||
"Competitive",
|
|
||||||
"Adventure",
|
|
||||||
"Snarky",
|
|
||||||
]);
|
|
||||||
|
|
||||||
export const units = ref(["Imperial", "Metric"]);
|
export const tones = ref(availableTones);
|
||||||
|
|
||||||
|
export const units = ref(availableUnits);
|
||||||
|
|||||||
Reference in New Issue
Block a user