init
This commit is contained in:
@@ -28,9 +28,15 @@ Jeśli podano dane z poprzednich treningów, odnieś się do progresu (np. zmian
|
||||
type PreviousRun = { run: RunningActivity; analysis: AiAnalysis | null };
|
||||
type PreviousWorkout = { workout: StrengthWorkout; analysis: AiAnalysis | null };
|
||||
|
||||
function secToMinKm(sec: number): string {
|
||||
const m = Math.floor(sec / 60);
|
||||
const s = Math.round(sec % 60);
|
||||
return `${m}:${s.toString().padStart(2, "0")} min/km`;
|
||||
}
|
||||
|
||||
function buildRunMetricsSummary(metrics: RunMetrics, totalDistanceM: number): string[] {
|
||||
const { distanceKm, hrBpm, gcbLeftPct } = metrics;
|
||||
if (!hrBpm && !gcbLeftPct) return [];
|
||||
const { distanceKm, hrBpm, gcbLeftPct, paceSec } = metrics;
|
||||
if (!hrBpm && !gcbLeftPct && !paceSec) return [];
|
||||
const n = distanceKm.length;
|
||||
if (n < 8) return [];
|
||||
|
||||
@@ -60,6 +66,11 @@ function buildRunMetricsSummary(metrics: RunMetrics, totalDistanceM: number): st
|
||||
const a = avg(vals);
|
||||
if (a !== null) parts.push(`HR śr. ${a} bpm`);
|
||||
}
|
||||
if (paceSec) {
|
||||
const vals = idx.map((i) => paceSec[i]).filter((v) => v > 0 && v < 1800);
|
||||
const a = avg(vals);
|
||||
if (a !== null) parts.push(`tempo śr. ${secToMinKm(a)}`);
|
||||
}
|
||||
if (gcbLeftPct) {
|
||||
const vals = idx.map((i) => gcbLeftPct[i]).filter((v) => v > 0);
|
||||
if (vals.length > 0) {
|
||||
@@ -184,6 +195,7 @@ function parseAnalysisResponse(text: string): { summary: string; tips: string[]
|
||||
}
|
||||
|
||||
export async function generateAnalysis(
|
||||
userId: string,
|
||||
targetType: AiAnalysisTargetType,
|
||||
targetId: string
|
||||
): Promise<AiAnalysis> {
|
||||
@@ -194,28 +206,28 @@ export async function generateAnalysis(
|
||||
|
||||
let prompt: string;
|
||||
if (targetType === "running") {
|
||||
const activity = await getRunningActivity(targetId);
|
||||
const activity = await getRunningActivity(userId, targetId);
|
||||
if (!activity) throw new Error("Nie znaleziono biegu.");
|
||||
const previousRuns = (await listRunningActivities())
|
||||
const previousRuns = (await listRunningActivities(userId))
|
||||
.filter((run) => run.startTime < activity.startTime)
|
||||
.slice(0, PREVIOUS_RUNS_LIMIT);
|
||||
const previousRunsWithAnalysis: PreviousRun[] = await Promise.all(
|
||||
previousRuns.map(async (run) => ({
|
||||
run,
|
||||
analysis: await getLatestAnalysisForTarget("running", run._id),
|
||||
analysis: await getLatestAnalysisForTarget(userId, "running", run._id),
|
||||
}))
|
||||
);
|
||||
prompt = buildRunningPrompt(activity, previousRunsWithAnalysis);
|
||||
} else {
|
||||
const workout = await getStrengthWorkout(targetId);
|
||||
const workout = await getStrengthWorkout(userId, targetId);
|
||||
if (!workout) throw new Error("Nie znaleziono treningu.");
|
||||
const previousWorkouts = (await listStrengthWorkouts())
|
||||
const previousWorkouts = (await listStrengthWorkouts(userId))
|
||||
.filter((previous) => previous.date < workout.date)
|
||||
.slice(0, PREVIOUS_WORKOUTS_LIMIT);
|
||||
const previousWorkoutsWithAnalysis: PreviousWorkout[] = await Promise.all(
|
||||
previousWorkouts.map(async (previous) => ({
|
||||
workout: previous,
|
||||
analysis: await getLatestAnalysisForTarget("strength", previous._id),
|
||||
analysis: await getLatestAnalysisForTarget(userId, "strength", previous._id),
|
||||
}))
|
||||
);
|
||||
prompt = buildStrengthPrompt(workout, previousWorkoutsWithAnalysis);
|
||||
@@ -234,6 +246,7 @@ export async function generateAnalysis(
|
||||
const { summary, tips } = parseAnalysisResponse(text);
|
||||
|
||||
return saveAiAnalysis({
|
||||
userId,
|
||||
targetType,
|
||||
targetId: new ObjectId(targetId),
|
||||
summary,
|
||||
@@ -332,18 +345,18 @@ function buildDashboardPrompt(
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
export async function generateDashboardAnalysis(): Promise<AiAnalysis> {
|
||||
export async function generateDashboardAnalysis(userId: string): Promise<AiAnalysis> {
|
||||
const apiKey = process.env.ANTHROPIC_API_KEY;
|
||||
if (!apiKey) throw new Error("Brak klucza ANTHROPIC_API_KEY w konfiguracji.");
|
||||
|
||||
const [runs, workouts] = await Promise.all([
|
||||
listRunningActivities().then((r) => r.slice(0, DASHBOARD_RUNS_LIMIT)),
|
||||
listStrengthWorkouts().then((w) => w.slice(0, DASHBOARD_WORKOUTS_LIMIT)),
|
||||
listRunningActivities(userId).then((r) => r.slice(0, DASHBOARD_RUNS_LIMIT)),
|
||||
listStrengthWorkouts(userId).then((w) => w.slice(0, DASHBOARD_WORKOUTS_LIMIT)),
|
||||
]);
|
||||
|
||||
let wellness: DayWellness[] = [];
|
||||
try {
|
||||
const garminClient = await getAuthorizedClient();
|
||||
const garminClient = await getAuthorizedClient(userId);
|
||||
wellness = await fetchRecentWellness(garminClient, DASHBOARD_WELLNESS_DAYS);
|
||||
} catch {
|
||||
// Wellness data not available, proceed without it
|
||||
@@ -361,5 +374,5 @@ export async function generateDashboardAnalysis(): Promise<AiAnalysis> {
|
||||
const textBlock = message.content.find((b) => b.type === "text");
|
||||
const text = textBlock && textBlock.type === "text" ? textBlock.text : "";
|
||||
const { summary, tips } = parseAnalysisResponse(text);
|
||||
return saveDashboardAnalysis(summary, tips, model);
|
||||
return saveDashboardAnalysis(userId, summary, tips, model);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user