Change font
This commit is contained in:
@@ -2,6 +2,8 @@
|
|||||||
@import "@nuxt/ui";
|
@import "@nuxt/ui";
|
||||||
|
|
||||||
@theme static {
|
@theme static {
|
||||||
|
--font-sans: "Geist", sans-serif;
|
||||||
|
|
||||||
--ui-primary: var(--color-orange-500);
|
--ui-primary: var(--color-orange-500);
|
||||||
|
|
||||||
--ui-color-primary-50: var(--color-orange-50);
|
--ui-color-primary-50: var(--color-orange-50);
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ const { user, clear } = useUserSession();
|
|||||||
<UContainer class="max-w-2xl flex justify-between items-center">
|
<UContainer class="max-w-2xl flex justify-between items-center">
|
||||||
<div class="flex gap-3 items-center">
|
<div class="flex gap-3 items-center">
|
||||||
<NuxtImg src="/strivify.png" width="36" height="36" />
|
<NuxtImg src="/strivify.png" width="36" height="36" />
|
||||||
<div class="font-bold text-xl">Strivify</div>
|
<div class="font-bold text-xl tracking-tight">Strivify</div>
|
||||||
</div>
|
</div>
|
||||||
<UDropdownMenu :items="[{ label: 'Log out', onSelect: () => clear() }]">
|
<UDropdownMenu :items="[{ label: 'Log out', onSelect: () => clear() }]">
|
||||||
<UAvatar
|
<UAvatar
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import AppFooter from "./app-footer.vue";
|
import AppFooter from "./app-footer.vue";
|
||||||
|
|
||||||
|
useHead({ title: "Strivify - Please sign in" });
|
||||||
|
|
||||||
const { openInPopup } = useUserSession();
|
const { openInPopup } = useUserSession();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
useHead({ title: "Strivify" });
|
||||||
|
|
||||||
const { user } = useUserSession();
|
const { user } = useUserSession();
|
||||||
|
|
||||||
const stravaLink = computed(() => {
|
const stravaLink = computed(() => {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export default defineNuxtConfig({
|
|||||||
runtimeConfig: {
|
runtimeConfig: {
|
||||||
webhooksUrl: "",
|
webhooksUrl: "",
|
||||||
stravaVerifyToken: "",
|
stravaVerifyToken: "",
|
||||||
|
openaiApiKey: "",
|
||||||
},
|
},
|
||||||
future: { compatibilityVersion: 4 },
|
future: { compatibilityVersion: 4 },
|
||||||
compatibilityDate: "2025-03-01",
|
compatibilityDate: "2025-03-01",
|
||||||
@@ -19,6 +20,7 @@ export default defineNuxtConfig({
|
|||||||
ai: true,
|
ai: true,
|
||||||
database: true,
|
database: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
app: {
|
app: {
|
||||||
head: {
|
head: {
|
||||||
link: [
|
link: [
|
||||||
|
|||||||
7308
package-lock.json
generated
7308
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,10 +1,14 @@
|
|||||||
import { get, omit, tryit } from "radash";
|
import { get, omit } from "radash";
|
||||||
|
import { OpenAI } from "openai";
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
const body = await readBody(event);
|
const body = await readBody(event);
|
||||||
const db = useDrizzle();
|
const db = useDrizzle();
|
||||||
|
const config = useRuntimeConfig();
|
||||||
|
|
||||||
const ai = hubAI();
|
const openai = new OpenAI({
|
||||||
|
apiKey: config.openaiApiKey,
|
||||||
|
});
|
||||||
|
|
||||||
const user = await db.query.users.findFirst({
|
const user = await db.query.users.findFirst({
|
||||||
where: (f, o) => o.eq(f.id, body.owner_id),
|
where: (f, o) => o.eq(f.id, body.owner_id),
|
||||||
@@ -19,35 +23,14 @@ export default defineEventHandler(async (event) => {
|
|||||||
|
|
||||||
const strava = await useStrava(body.owner_id);
|
const strava = await useStrava(body.owner_id);
|
||||||
|
|
||||||
const activity = (await strava!(`/activities/${body.object_id}`)) as any;
|
const [, activity] = await strava!<any>(`/activities/${body.object_id}`);
|
||||||
|
|
||||||
const promptActivity = `
|
const aiResponse = await openai.chat.completions.create({
|
||||||
type: ${get(activity, "type")}
|
model: "gpt-4o-mini",
|
||||||
distance: ${get(activity, "distance")}m
|
messages: [
|
||||||
moving time: ${get(activity, "moving_time")}sec
|
|
||||||
elapsed time: ${get(activity, "elapsed_time")}sec
|
|
||||||
total elevation gain: ${get(activity, "total_elevation_gain")}m
|
|
||||||
start (local): ${get(activity, "start_date_local")}
|
|
||||||
trainer: ${get(activity, "trainer")}
|
|
||||||
commute: ${get(activity, "commute")}
|
|
||||||
calories: ${get(activity, "calories")}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const [aiError, aiResponse] = await tryit(ai.run)(
|
|
||||||
"@cf/meta/llama-3.1-8b-instruct",
|
|
||||||
{
|
{
|
||||||
response_format: {
|
role: "user",
|
||||||
type: "json_schema",
|
content: `
|
||||||
json_schema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
title: "string",
|
|
||||||
description: "string",
|
|
||||||
},
|
|
||||||
required: ["title", "description"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
prompt: `
|
|
||||||
Generate a title and a short description for my strava activity. Use my preferred language.
|
Generate a title and a short description for my strava activity. Use my preferred language.
|
||||||
Use ${user?.preferences.data.tone} tone to generate content.
|
Use ${user?.preferences.data.tone} tone to generate content.
|
||||||
Add emojis unless tone is set to minimalist.
|
Add emojis unless tone is set to minimalist.
|
||||||
@@ -57,27 +40,76 @@ export default defineEventHandler(async (event) => {
|
|||||||
Weight: ${user?.weight}
|
Weight: ${user?.weight}
|
||||||
Language: ${user?.preferences.data.language}
|
Language: ${user?.preferences.data.language}
|
||||||
|
|
||||||
The activity data:
|
Activity notes:
|
||||||
${promptActivity}
|
distance is in meters, time is in seconds, don't include average speed. Convert time to hours or minutes, whatever's closer.
|
||||||
`,
|
The activity data in json format from strava:
|
||||||
},
|
${JSON.stringify(
|
||||||
);
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
omit(activity, [
|
omit(activity, [
|
||||||
"map",
|
|
||||||
"laps",
|
"laps",
|
||||||
|
"segment_efforts",
|
||||||
|
"splits_metric",
|
||||||
|
"splits_standard",
|
||||||
|
"hide_from_home",
|
||||||
|
"available_zones",
|
||||||
|
"map",
|
||||||
|
"start_date_local",
|
||||||
|
"gear",
|
||||||
"stats_visibility",
|
"stats_visibility",
|
||||||
"embed_token",
|
"embed_token",
|
||||||
"private_note",
|
"name",
|
||||||
|
"description",
|
||||||
]),
|
]),
|
||||||
);
|
)}
|
||||||
|
`,
|
||||||
await strava!(`activities/${body.object_id}`, {
|
},
|
||||||
method: "PUT",
|
],
|
||||||
body: {
|
tools: [
|
||||||
name: get(aiResponse, "response.title"),
|
{
|
||||||
description: get(aiResponse, "response.description"),
|
type: "function",
|
||||||
|
function: {
|
||||||
|
name: "generate_strava_meta",
|
||||||
|
description: "Generates Strava metadata.",
|
||||||
|
parameters: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
title: {
|
||||||
|
type: "string",
|
||||||
|
description: "The title of the activity",
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
type: "string",
|
||||||
|
description: "A short description of the activity",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ["title", "description"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
tool_choice: {
|
||||||
|
type: "function",
|
||||||
|
function: {
|
||||||
|
name: "generate_strava_meta",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const responseObject = JSON.parse(
|
||||||
|
get(aiResponse, "choices.0.message.tool_calls.0.function.arguments"),
|
||||||
|
) as { title: string; description: string };
|
||||||
|
|
||||||
|
const [stravaError] = await strava!(`activities/${body.object_id}`, {
|
||||||
|
method: "PUT",
|
||||||
|
body: {
|
||||||
|
name: responseObject.title,
|
||||||
|
description: responseObject.description,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (stravaError) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 500,
|
||||||
|
message: `Strava API: ${stravaError.message}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { get, isEmpty } from "radash";
|
import { get, isEmpty, tryit } from "radash";
|
||||||
import { isAfter } from "@formkit/tempo";
|
import { isAfter } from "@formkit/tempo";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import { URLSearchParams } from "url";
|
import { URLSearchParams } from "url";
|
||||||
@@ -57,10 +57,12 @@ export const useStrava = async (userId: number) => {
|
|||||||
.where(eq(tables.tokens.userId, userId));
|
.where(eq(tables.tokens.userId, userId));
|
||||||
}
|
}
|
||||||
|
|
||||||
return $fetch.create({
|
return tryit(
|
||||||
|
$fetch.create({
|
||||||
baseURL: "https://www.strava.com/api/v3/",
|
baseURL: "https://www.strava.com/api/v3/",
|
||||||
onRequest({ options }) {
|
onRequest({ options }) {
|
||||||
options.headers.set("Authorization", `Bearer ${tokens?.accessToken}`);
|
options.headers.set("Authorization", `Bearer ${tokens?.accessToken}`);
|
||||||
},
|
},
|
||||||
});
|
}),
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user