Skip to content

feat(Productivity App): Replace locale selector in productivity app example by a duration selector#628

Open
udaykakade25 wants to merge 3 commits intoalpic-ai:mainfrom
udaykakade25:selectorProductivityApp
Open

feat(Productivity App): Replace locale selector in productivity app example by a duration selector#628
udaykakade25 wants to merge 3 commits intoalpic-ai:mainfrom
udaykakade25:selectorProductivityApp

Conversation

@udaykakade25
Copy link
Copy Markdown
Contributor

@udaykakade25 udaykakade25 commented Mar 30, 2026

Closes #366

Changes

skybridge\examples\productivity\server\src\index.ts

  • Removed useState import — no more mutable locale state
  • Removed supportedLanguages export — no longer needed
  • useIntl() now derives lang directly from useUser().locale with no override possible
  • Returns only t() — locale and setLocale are gone

server/src/index.ts

  • Added getWeeks() function that aggregates N weeks by averaging each day's activity hours across
    for duration select

Result

Before

Screenshot (150)

After

Screenshot (151)

I used Claude Code heavily for this PR.

Greptile Summary

This PR replaces the manual locale selector in the productivity widget with a duration selector (1, 2, or 4 weeks), and simplifies locale handling by deriving it directly from useUser().locale instead of maintaining mutable state. The getWeeks() aggregation logic on the server is well-implemented and correctly averages daily activity hours across multiple weeks.

Key findings:

  • P1 — Stale widget state on direct tool re-invocation: The useEffect sync guard uses a ref (lastSyncedInputOffset) that only tracks weekOffset. If the AI directly calls the tool with the same weekOffset but a different duration, the guard prevents the updated output from being written to widgetState, leaving the widget showing stale data. The dropdown path (goToWeekonSuccess) is correct; only the direct-call sync path is broken.
  • P2 — Schema permits duration: 3: The Zod schema uses .min(1).max(4), which allows the value 3. The UI only offers [1, 2, 4], so a tool call with duration: 3 (valid per the schema) would result in the dropdown rendering with no matching option. Restricting the schema to a union of literals (1 | 2 | 4) would align the server contract with the UI.
  • P2 — Stale CSS class: The duration <select> still carries className=\"lang-select\" from the old locale selector.

Confidence Score: 4/5

Safe to merge after addressing the stale sync bug that prevents widget state updates when the AI changes only the duration on the same weekOffset.

One P1 logic bug: the sync guard in useEffect only checks weekOffset, so AI-driven duration changes on the same offset silently drop the new output. The remaining issues (schema literal set, stale CSS class) are P2. The aggregation logic itself is correct and the i18n.ts simplification is clean.

examples/productivity/web/src/widgets/show-productivity-insights.tsx — the useEffect sync guard needs to track both weekOffset and duration.

Important Files Changed

Filename Overview
examples/productivity/web/src/widgets/show-productivity-insights.tsx Replaces locale selector with duration dropdown. The onSuccess path in goToWeek handles dropdown-driven changes correctly, but the useEffect sync guard only checks weekOffset, causing stale state when the AI changes duration while keeping the same weekOffset.
examples/productivity/server/src/index.ts Adds getWeeks() aggregation function and duration schema parameter. Averaging logic is correct, but the schema allows duration values (e.g. 3) that the UI doesn't handle.
examples/productivity/web/src/i18n.ts Clean simplification — removes mutable locale state and the unused supportedLanguages export; locale is now derived directly from useUser().
examples/productivity/README.md Documentation updated to reflect the new duration selection feature and the removal of the manual locale selector.

Comments Outside Diff (1)

  1. examples/productivity/web/src/widgets/show-productivity-insights.tsx, line 54-59 (link)

    P1 Stale widget state when duration changes with same weekOffset

    The lastSyncedInputOffset ref only guards on weekOffset. When the AI directly re-invokes the tool with the same weekOffset but a different duration (e.g. going from {weekOffset: 0, duration: 1} to {weekOffset: 0, duration: 2}), the guard lastSyncedInputOffset.current !== weekOffset evaluates to 0 !== 0 → false, so the effect exits early and widgetState is never updated with the new aggregated output. The widget continues showing stale data from the previous call.

    The onSuccess path in goToWeek handles the dropdown interaction correctly, but this effect is the only mechanism for syncing state when the AI directly calls the tool — and it is now blind to duration changes.

    Consider tracking duration alongside weekOffset in the ref so the guard fires whenever either navigation parameter changes:

    const lastSyncedInput = useRef<{ weekOffset: number; duration: number } | null>(null);
    
    useEffect(() => {
      const weekOffset = input?.weekOffset ?? 0;
      const duration = input?.duration ?? 1;
      if (
        isSuccess &&
        output &&
        (lastSyncedInput.current?.weekOffset !== weekOffset ||
          lastSyncedInput.current?.duration !== duration)
      ) {
        lastSyncedInput.current = { weekOffset, duration };
        setWidgetState({ weekOffset, duration: duration as Duration, ...output });
      }
    }, [isSuccess, input, output, setWidgetState]);
    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: examples/productivity/web/src/widgets/show-productivity-insights.tsx
    Line: 54-59
    
    Comment:
    **Stale widget state when duration changes with same weekOffset**
    
    The `lastSyncedInputOffset` ref only guards on `weekOffset`. When the AI directly re-invokes the tool with the same `weekOffset` but a different `duration` (e.g. going from `{weekOffset: 0, duration: 1}` to `{weekOffset: 0, duration: 2}`), the guard `lastSyncedInputOffset.current !== weekOffset` evaluates to `0 !== 0 → false`, so the effect exits early and `widgetState` is never updated with the new aggregated output. The widget continues showing stale data from the previous call.
    
    The `onSuccess` path in `goToWeek` handles the *dropdown* interaction correctly, but this effect is the only mechanism for syncing state when the AI directly calls the tool — and it is now blind to duration changes.
    
    Consider tracking `duration` alongside `weekOffset` in the ref so the guard fires whenever either navigation parameter changes:
    
    ```ts
    const lastSyncedInput = useRef<{ weekOffset: number; duration: number } | null>(null);
    
    useEffect(() => {
      const weekOffset = input?.weekOffset ?? 0;
      const duration = input?.duration ?? 1;
      if (
        isSuccess &&
        output &&
        (lastSyncedInput.current?.weekOffset !== weekOffset ||
          lastSyncedInput.current?.duration !== duration)
      ) {
        lastSyncedInput.current = { weekOffset, duration };
        setWidgetState({ weekOffset, duration: duration as Duration, ...output });
      }
    }, [isSuccess, input, output, setWidgetState]);
    ```
    
    How can I resolve this? If you propose a fix, please make it concise.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: examples/productivity/web/src/widgets/show-productivity-insights.tsx
Line: 54-59

Comment:
**Stale widget state when duration changes with same weekOffset**

The `lastSyncedInputOffset` ref only guards on `weekOffset`. When the AI directly re-invokes the tool with the same `weekOffset` but a different `duration` (e.g. going from `{weekOffset: 0, duration: 1}` to `{weekOffset: 0, duration: 2}`), the guard `lastSyncedInputOffset.current !== weekOffset` evaluates to `0 !== 0 → false`, so the effect exits early and `widgetState` is never updated with the new aggregated output. The widget continues showing stale data from the previous call.

The `onSuccess` path in `goToWeek` handles the *dropdown* interaction correctly, but this effect is the only mechanism for syncing state when the AI directly calls the tool — and it is now blind to duration changes.

Consider tracking `duration` alongside `weekOffset` in the ref so the guard fires whenever either navigation parameter changes:

```ts
const lastSyncedInput = useRef<{ weekOffset: number; duration: number } | null>(null);

useEffect(() => {
  const weekOffset = input?.weekOffset ?? 0;
  const duration = input?.duration ?? 1;
  if (
    isSuccess &&
    output &&
    (lastSyncedInput.current?.weekOffset !== weekOffset ||
      lastSyncedInput.current?.duration !== duration)
  ) {
    lastSyncedInput.current = { weekOffset, duration };
    setWidgetState({ weekOffset, duration: duration as Duration, ...output });
  }
}, [isSuccess, input, output, setWidgetState]);
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: examples/productivity/server/src/index.ts
Line: 105-113

Comment:
**Schema allows `duration: 3` but UI only handles 1, 2, 4**

`z.number().min(1).max(4)` permits any integer 1–4, including `3`. The description text even reinforces the intended values ("1 = one week, 2 = two weeks, 4 = four weeks"), but nothing in the schema actually restricts the AI to those three values. If the model passes `duration: 3`, the aggregation in `getWeeks` works correctly, but the frontend dropdown (`value={widgetState.duration}`) has no matching `<option>` and will render with a blank/default-selected state.

Consider using a `z.union` of literals to enforce the exact set:

```ts
duration: z
  .union([z.literal(1), z.literal(2), z.literal(4)])
  .optional()
  .default(1)
  .describe("Number of weeks to aggregate (1, 2, or 4)"),
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: examples/productivity/web/src/widgets/show-productivity-insights.tsx
Line: 93-94

Comment:
**Stale CSS class name `lang-select`**

The `className` still reads `"lang-select"` even though this element is now a duration selector. This will make the CSS harder to maintain and is misleading for anyone reading the stylesheet.

```suggestion
            className="duration-select"
```

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "Update docs" | Re-trigger Greptile

Greptile also left 2 inline comments on this PR.

Comment on lines +105 to +113
duration: z
.number()
.min(1)
.max(4)
.optional()
.default(1)
.describe(
"Number of weeks to aggregate (1 = one week, 2 = two weeks, 4 = four weeks)",
),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Schema allows duration: 3 but UI only handles 1, 2, 4

z.number().min(1).max(4) permits any integer 1–4, including 3. The description text even reinforces the intended values ("1 = one week, 2 = two weeks, 4 = four weeks"), but nothing in the schema actually restricts the AI to those three values. If the model passes duration: 3, the aggregation in getWeeks works correctly, but the frontend dropdown (value={widgetState.duration}) has no matching <option> and will render with a blank/default-selected state.

Consider using a z.union of literals to enforce the exact set:

duration: z
  .union([z.literal(1), z.literal(2), z.literal(4)])
  .optional()
  .default(1)
  .describe("Number of weeks to aggregate (1, 2, or 4)"),
Prompt To Fix With AI
This is a comment left during a code review.
Path: examples/productivity/server/src/index.ts
Line: 105-113

Comment:
**Schema allows `duration: 3` but UI only handles 1, 2, 4**

`z.number().min(1).max(4)` permits any integer 1–4, including `3`. The description text even reinforces the intended values ("1 = one week, 2 = two weeks, 4 = four weeks"), but nothing in the schema actually restricts the AI to those three values. If the model passes `duration: 3`, the aggregation in `getWeeks` works correctly, but the frontend dropdown (`value={widgetState.duration}`) has no matching `<option>` and will render with a blank/default-selected state.

Consider using a `z.union` of literals to enforce the exact set:

```ts
duration: z
  .union([z.literal(1), z.literal(2), z.literal(4)])
  .optional()
  .default(1)
  .describe("Number of weeks to aggregate (1, 2, or 4)"),
```

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a real issue for now

Comment thread examples/productivity/web/src/widgets/show-productivity-insights.tsx Outdated
- **Theme Support**: Adapts to light/dark mode using the `useLayout()` hook
- **Localization**: Translates UI based on user locale via `useUser()` hook (English, French, Spanish, Chinese)
- **Localization**: Automatically translates UI based on user locale via `useUser()` hook (English, French, Spanish, Chinese) — no manual selector needed
- **Duration Selection**: Aggregates productivity data across 1, 2, or 4 weeks using a header dropdown, powered by the `duration` tool parameter
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not relevant - it should only showcase the skybridge APIs

# Productivity Example

An example MCP app built with [Skybridge](https://docs.skybridge.tech/home): interactive productivity charts with localization, display mode, follow-up messages, and widget-to-tool communication.
An example MCP app built with [Skybridge](https://docs.skybridge.tech/home): interactive productivity charts with duration selection, localization, display mode, follow-up messages, and widget-to-tool communication.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here

Copy link
Copy Markdown
Contributor

@qchuchu qchuchu left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few comments to improve. You can also rebase - we had an issue on a non up to date lock file. Thanks for contributing !

@qchuchu qchuchu self-assigned this Mar 30, 2026
…ts.tsx

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Replace locale selector in productivity app example by a duration selector

2 participants