forked from BasedHardware/omi
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathuseGoalDetail.ts
More file actions
91 lines (82 loc) · 2.74 KB
/
Copy pathuseGoalDetail.ts
File metadata and controls
91 lines (82 loc) · 2.74 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
'use client';
import { useCallback, useMemo, useState } from 'react';
import { getGoalAdvice, getGoalHistory } from '@/lib/api';
import { useAsyncResource } from '@/hooks/useAsyncResource';
import { useRequestOwner } from '@/hooks/useRequestOwner';
import type { GoalHistoryEntry } from '@/types/goals';
export interface UseGoalDetailReturn {
history: GoalHistoryEntry[];
historyLoading: boolean;
historyError: string | null;
advice: string | null;
adviceLoading: boolean;
adviceError: string | null;
requestAdvice: () => Promise<void>;
}
const NO_HISTORY: GoalHistoryEntry[] = [];
/**
* History for one goal, plus advice fetched only on demand.
*
* Advice is an LLM call behind a server-side rate limit, so it never runs on
* open — only when the user asks for it. It is therefore plain state rather
* than a resource, which would fetch eagerly.
*/
export function useGoalDetail(goalId: string | null): UseGoalDetailReturn {
const history = useAsyncResource(
goalId,
useCallback(() => getGoalHistory(goalId as string), [goalId]),
{ fallbackMessage: 'Failed to load history' },
);
const [advice, setAdvice] = useState<string | null>(null);
const [adviceLoading, setAdviceLoading] = useState(false);
const [adviceError, setAdviceError] = useState<string | null>(null);
const [adviceGoalId, setAdviceGoalId] = useState<string | null>(goalId);
// Advice belongs to the goal it was requested for; drop it when the
// selection changes rather than showing it under a new title. The in-flight
// request goes with it: the new goal starts idle, not stuck behind the old
// goal's pending call.
if (goalId !== adviceGoalId) {
setAdviceGoalId(goalId);
setAdvice(null);
setAdviceError(null);
setAdviceLoading(false);
}
const claimRequest = useRequestOwner(goalId);
const requestAdvice = useCallback(async () => {
if (!goalId) return;
const isCurrent = claimRequest();
setAdviceLoading(true);
setAdviceError(null);
try {
const result = await getGoalAdvice(goalId);
if (!isCurrent()) return;
setAdvice(result);
} catch (err) {
if (!isCurrent()) return;
console.error('Failed to load goal advice:', err);
setAdviceError(err instanceof Error ? err.message : 'Failed to load advice');
} finally {
if (isCurrent()) setAdviceLoading(false);
}
}, [goalId, claimRequest]);
return useMemo(
() => ({
history: history.data ?? NO_HISTORY,
historyLoading: history.loading,
historyError: history.error,
advice,
adviceLoading,
adviceError,
requestAdvice,
}),
[
history.data,
history.loading,
history.error,
advice,
adviceLoading,
adviceError,
requestAdvice,
],
);
}