Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
b1057de
backend integration
Xavier-Charles Nov 17, 2025
99dab7e
add showRecipe toggle
Xavier-Charles Nov 18, 2025
6098176
setup migration
Xavier-Charles Nov 18, 2025
6c39a34
Add RecipeModal component and container for recipe management
Xavier-Charles Nov 19, 2025
ee3ab21
Simplify category handling in RecipeModal component
Xavier-Charles Nov 19, 2025
82e7106
RecipeModalContainer to use toggleRecipeModal action
Xavier-Charles Nov 19, 2025
1962e1b
remove createRecipeTemplate endpoint and related types
Xavier-Charles Nov 19, 2025
6263f9a
Add RecipeForm and RecipeModal components for recipe creation and man…
Xavier-Charles Nov 19, 2025
36b01fd
Enhance RecipeForm and RecipeModal components to support recipe editi…
Xavier-Charles Nov 19, 2025
a95810b
register widget
Xavier-Charles Nov 20, 2025
6e6ba37
create recipe widget
Xavier-Charles Nov 20, 2025
9bac8f9
create recipe module
Xavier-Charles Nov 20, 2025
01d4701
UI improvments
Xavier-Charles Nov 20, 2025
95defea
Enhance RecipeModule and RecipeContainer to include recipe templates …
Xavier-Charles Nov 20, 2025
df965ef
Add RecipeItem component for individual recipe management, integratin…
Xavier-Charles Nov 20, 2025
d5b19a6
Update RecipeModalContainer to implement new mutation hooks for mana…
Xavier-Charles Nov 21, 2025
dc7adae
Implement authentication checks in RecipeModalContainer and RecipeCon…
Xavier-Charles Nov 21, 2025
087ddf1
update type definition
Xavier-Charles Nov 21, 2025
07a5ce1
improve activate toggle
Xavier-Charles Nov 21, 2025
d7d401a
use hash for recipe modal
Xavier-Charles Nov 21, 2025
bbf001b
clean up
Xavier-Charles Nov 21, 2025
75a2aa4
Add delivery channels display in RecipeItem component
Xavier-Charles Nov 21, 2025
41f8e31
humanize schedule
Xavier-Charles Nov 21, 2025
64710cb
- Introduced new API endpoint for fetching output formats.
Xavier-Charles Nov 21, 2025
5e53d8d
- Enhanced RecipeForm, RecipeItem, and RecipeModal to support output …
Xavier-Charles Nov 21, 2025
2c37f5b
- Integrated output formats into RecipeModule and RecipeContainer for…
Xavier-Charles Nov 21, 2025
7a1c5be
Added outputFormat handling in RecipeModal to utilize recipeData when…
Xavier-Charles Nov 21, 2025
7d77f1e
Refactor RecipeModal category selection styles to maintain background…
Xavier-Charles Nov 21, 2025
ad7352a
Update outputFormat type from number to string across Recipe componen…
Xavier-Charles Nov 21, 2025
c9ebdd2
Refactor time zone and day of week management by centralizing definit…
Xavier-Charles Nov 21, 2025
239a00a
Add trigger recipe functionality with new API endpoint and UI integra…
Xavier-Charles Nov 21, 2025
f86b501
clean up
Xavier-Charles Nov 21, 2025
e1d4d5d
Merge branch 'dev' into feat/recipes
Xavier-Charles Nov 25, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/frontend/src/api/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,4 @@ export * from "./usePullToRefresh";
export * from "./useHistory";
export * from "./useSelectedCoin";
export * from "./useImageWidget";
export * from "./useRecipeModalHash";
47 changes: 47 additions & 0 deletions packages/frontend/src/api/hooks/useRecipeModalHash.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { useCallback, useEffect, useState } from "react";

const RECIPE_HASH = "#recipes";

/**
* Hook to manage recipe modal state via URL hash.
* When `#recipes` is in the URL, the modal is open.
* Removing the hash closes the modal.
*/
export const useRecipeModalHash = () => {
const [showModal, setShowModal] = useState(
() => window.location.hash === RECIPE_HASH
);

useEffect(() => {
const handleHashChange = () => {
setShowModal(window.location.hash === RECIPE_HASH);
};

window.addEventListener("hashchange", handleHashChange);
return () => window.removeEventListener("hashchange", handleHashChange);
}, []);

const openModal = useCallback(() => {
window.location.hash = RECIPE_HASH;
}, []);

const closeModal = useCallback(() => {
// Remove hash without triggering a page jump
window.history.pushState(
null,
"",
window.location.pathname + window.location.search
);
setShowModal(false);
}, []);

const toggleModal = useCallback(() => {
if (showModal) {
closeModal();
} else {
openModal();
}
}, [showModal, closeModal, openModal]);

return { showModal, openModal, closeModal, toggleModal };
};
320 changes: 320 additions & 0 deletions packages/frontend/src/api/services/recipes/recipeEndpoints.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,320 @@
import queryString from "query-string";
import {
TOutputFormat,
TRecipe,
TRecipeOutput,
TRecipeSource,
TRecipeTemplate,
} from "src/api/types";
import { Logger } from "src/api/utils/logging";
import CONFIG from "../../../config/config";
import { alphadayApi } from "../alphadayApi";
import {
TGetRecipeTemplatesRequest,
TGetRecipeTemplatesRawResponse,
TGetRecipeTemplatesResponse,
TRecipeTemplateRaw,
TGetRecipesRequest,
TGetRecipesRawResponse,
TGetRecipesResponse,
TCreateRecipeRequest,
TCreateRecipeRequestRaw,
TCreateRecipeResponse,
TCreateRecipeRawResponse,
TGetRecipeRequest,
TGetRecipeResponse,
TGetRecipeRawResponse,
TUpdateRecipeRequest,
TUpdateRecipeRequestRaw,
TUpdateRecipeResponse,
TUpdateRecipeRawResponse,
TActivateRecipeRequest,
TActivateRecipeResponse,
TActivateRecipeRawResponse,
TDeactivateRecipeRequest,
TDeactivateRecipeResponse,
TDeactivateRecipeRawResponse,
TTriggerRecipeRequest,
TTriggerRecipeResponse,
TTriggerRecipeRawResponse,
TRecipeRaw,
TRecipeSourceRaw,
TRecipeOutputRaw,
TGetOutputFormatsRequest,
TGetOutputFormatsRawResponse,
TGetOutputFormatsResponse,
TOutputFormatRaw,
} from "./types";

const { RECIPES } = CONFIG.API.DEFAULT.ROUTES;

const transformRecipeTemplate = (raw: TRecipeTemplateRaw): TRecipeTemplate => ({
id: raw.id,
name: raw.name,
description: raw.description,
category: raw.category,
author: raw.author,
authorEmail: raw.author_email,
isPublic: raw.is_public,
isFeatured: raw.is_featured,
usageCount: raw.usage_count,
templateConfig: raw.template_config,
previewImage: raw.preview_image,
tags: raw.tags,
created: raw.created,
modified: raw.modified,
});

const transformRecipeSource = (raw: TRecipeSourceRaw): TRecipeSource => ({
id: raw.id,
sourceCategory: raw.source_category,
filters: raw.filters,
maxItems: raw.max_items,
priorityScoreThreshold: raw.priority_score_threshold,
created: raw.created,
modified: raw.modified,
});

const transformRecipeOutput = (raw: TRecipeOutputRaw): TRecipeOutput => ({
id: raw.id,
outputFormat: raw.output_format,
outputFormatName: raw.output_format_name,
promptTemplate: raw.prompt_template,
promptTemplateName: raw.prompt_template_name,
userPromptOverride: raw.user_prompt_override,
deliveryChannels: raw.delivery_channels,
created: raw.created,
modified: raw.modified,
});

const transformOutputFormat = (raw: TOutputFormatRaw): TOutputFormat => ({
id: raw.id,
type: raw.type,
name: raw.name,
description: raw.description,
template: raw.template,
costMultiplier: raw.cost_multiplier,
isActive: raw.is_active,
created: raw.created,
modified: raw.modified,
});

const transformRecipe = (raw: TRecipeRaw): TRecipe => ({
id: raw.id,
user: raw.user,
userEmail: raw.user_email,
name: raw.name,
description: raw.description,
isActive: raw.is_active,
schedule: raw.schedule,
lastRun: raw.last_run,
timezone: raw.timezone,
version: raw.version,
recipeSources: raw.recipe_sources?.map(transformRecipeSource),
recipeOutputs: raw.recipe_outputs?.map(transformRecipeOutput),
created: raw.created,
modified: raw.modified,
});

const recipesApi = alphadayApi.injectEndpoints({
endpoints: (builder) => ({
getRecipeTemplates: builder.query<
TGetRecipeTemplatesResponse,
TGetRecipeTemplatesRequest
>({
query: (req) => {
const params: string = queryString.stringify(req);
const path = `${RECIPES.BASE}${RECIPES.TEMPLATES}?${params}`;
Logger.debug("getRecipeTemplates: querying", path);
return path;
},
transformResponse: (
r: TGetRecipeTemplatesRawResponse
): TGetRecipeTemplatesResponse => ({
...r,
results: r.results.map(transformRecipeTemplate),
}),
keepUnusedDataFor: 0,
}),
getRecipes: builder.query<TGetRecipesResponse, TGetRecipesRequest>({
query: (req) => {
const params: string = queryString.stringify(req);
const path = `${RECIPES.BASE}${RECIPES.LIST}?${params}`;
Logger.debug("getRecipes: querying", path);
return path;
},
transformResponse: (
r: TGetRecipesRawResponse
): TGetRecipesResponse => ({
...r,
results: r.results.map(transformRecipe),
}),
keepUnusedDataFor: 0,
}),
getRecipe: builder.query<TGetRecipeResponse, TGetRecipeRequest>({
query: (req) => {
const path = `${RECIPES.BASE}${RECIPES.BY_ID(req.id)}`;
Logger.debug("getRecipe: querying", path);
return path;
},
transformResponse: (r: TGetRecipeRawResponse): TGetRecipeResponse =>
transformRecipe(r),
keepUnusedDataFor: 0,
}),
createRecipe: builder.mutation<
TCreateRecipeResponse,
TCreateRecipeRequest
>({
query: (req: TCreateRecipeRequest) => {
const body: TCreateRecipeRequestRaw = {
name: req.name,
description: req.description,
is_active: req.isActive,
schedule: req.schedule,
timezone: req.timezone,
sources: req.sources.map((s) => ({
source_category: s.sourceCategory,
filters: s.filters,
max_items: s.maxItems,
priority_score_threshold: s.priorityScoreThreshold,
})),
outputs: req.outputs.map((o) => ({
output_format: o.outputFormat,
prompt_template: o.promptTemplate,
user_prompt_override: o.userPromptOverride,
delivery_channels: o.deliveryChannels,
})),
};
const path = `${RECIPES.BASE}${RECIPES.LIST}`;
Logger.debug("createRecipe: querying", path);
return {
url: path,
method: "POST",
body,
};
},
transformResponse: (
r: TCreateRecipeRawResponse
): TCreateRecipeResponse => transformRecipe(r),
}),
updateRecipe: builder.mutation<
TUpdateRecipeResponse,
TUpdateRecipeRequest
>({
query: (req: TUpdateRecipeRequest) => {
const { id, ...rest } = req;
const body: TUpdateRecipeRequestRaw = {
name: rest.name,
description: rest.description,
is_active: rest.isActive,
schedule: rest.schedule,
timezone: rest.timezone,
sources: rest.sources.map((s) => ({
source_category: s.sourceCategory,
filters: s.filters,
max_items: s.maxItems,
priority_score_threshold: s.priorityScoreThreshold,
})),
outputs: rest.outputs.map((o) => ({
output_format: o.outputFormat,
prompt_template: o.promptTemplate,
user_prompt_override: o.userPromptOverride,
delivery_channels: o.deliveryChannels,
})),
};
const path = `${RECIPES.BASE}${RECIPES.BY_ID(id)}`;
Logger.debug("updateRecipe: querying", path);
return {
url: path,
method: "PUT",
body,
};
},
transformResponse: (
r: TUpdateRecipeRawResponse
): TUpdateRecipeResponse => transformRecipe(r),
}),
activateRecipe: builder.mutation<
TActivateRecipeResponse,
TActivateRecipeRequest
>({
query: (req: TActivateRecipeRequest) => {
const path = `${RECIPES.BASE}${RECIPES.ACTIVATE(req.id)}`;
Logger.debug("activateRecipe: querying", path);
return {
url: path,
method: "POST",
body: {},
};
},
transformResponse: (
r: TActivateRecipeRawResponse
): TActivateRecipeResponse => transformRecipe(r),
}),
deactivateRecipe: builder.mutation<
TDeactivateRecipeResponse,
TDeactivateRecipeRequest
>({
query: (req: TDeactivateRecipeRequest) => {
const path = `${RECIPES.BASE}${RECIPES.DEACTIVATE(req.id)}`;
Logger.debug("deactivateRecipe: querying", path);
return {
url: path,
method: "POST",
body: {},
};
},
transformResponse: (
r: TDeactivateRecipeRawResponse
): TDeactivateRecipeResponse => transformRecipe(r),
}),
triggerRecipe: builder.mutation<
TTriggerRecipeResponse,
TTriggerRecipeRequest
>({
query: (req: TTriggerRecipeRequest) => {
const path = `${RECIPES.BASE}${RECIPES.TRIGGER(req.id)}`;
Logger.debug("triggerRecipe: querying", path);
return {
url: path,
method: "POST",
body: {},
};
},
transformResponse: (
r: TTriggerRecipeRawResponse
): TTriggerRecipeResponse => transformRecipe(r),
}),
getOutputFormats: builder.query<
TGetOutputFormatsResponse,
TGetOutputFormatsRequest
>({
query: (req) => {
const params: string = queryString.stringify(req);
const path = `${RECIPES.BASE}${RECIPES.OUTPUT_FORMATS}?${params}`;
Logger.debug("getOutputFormats: querying", path);
return path;
},
transformResponse: (
r: TGetOutputFormatsRawResponse
): TGetOutputFormatsResponse => ({
...r,
results: r.results.map(transformOutputFormat),
}),
keepUnusedDataFor: 300,
}),
}),
overrideExisting: false,
});

export const {
useGetRecipeTemplatesQuery,
useGetRecipesQuery,
useGetRecipeQuery,
useCreateRecipeMutation,
useUpdateRecipeMutation,
useActivateRecipeMutation,
useDeactivateRecipeMutation,
useTriggerRecipeMutation,
useGetOutputFormatsQuery,
} = recipesApi;
Loading
Loading