diff --git a/compose.override.yml b/compose.override.yml index 177a19d..0949b10 100644 --- a/compose.override.yml +++ b/compose.override.yml @@ -8,9 +8,9 @@ services: # Docker can follow the symlink inside the container. - /Library/Assets/kiwi:/Library/Assets/kiwi:rw - # cf-orch agent sidecar: registers kiwi as a GPU node with the coordinator. + # cf-orch agent sidecar: registers this machine as GPU node "sif" with the coordinator. # The API scheduler uses COORDINATOR_URL to lease VRAM cooperatively; this - # agent makes kiwi's VRAM usage visible on the orchestrator dashboard. + # agent makes the local VRAM usage visible on the orchestrator dashboard. cf-orch-agent: image: kiwi-api # reuse local api image — cf-core already installed there network_mode: host @@ -21,7 +21,7 @@ services: command: > conda run -n kiwi cf-orch agent --coordinator ${COORDINATOR_URL:-http://10.1.10.71:7700} - --node-id kiwi + --node-id sif --host 0.0.0.0 --port 7702 --advertise-host ${CF_ORCH_ADVERTISE_HOST:-10.1.10.71} diff --git a/frontend/src/components/RecipesView.vue b/frontend/src/components/RecipesView.vue index fd13cb7..7671b29 100644 --- a/frontend/src/components/RecipesView.vue +++ b/frontend/src/components/RecipesView.vue @@ -287,7 +287,10 @@ @click="handleSuggest" > - Finding recipes… + + Queued… + Generating… + Finding recipes… Suggest Recipes @@ -312,7 +315,9 @@
- Finding recipes… + Recipe request queued, waiting for model… + Generating your recipe now… + Finding recipes… {{ filteredSuggestions.length }} recipe{{ filteredSuggestions.length !== 1 ? 's' : '' }} found diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts index ef19ab6..acb9078 100644 --- a/frontend/src/services/api.ts +++ b/frontend/src/services/api.ts @@ -524,6 +524,15 @@ export interface RecipeResult { rate_limit_count: number } +export type RecipeJobStatusValue = 'queued' | 'running' | 'done' | 'failed' + +export interface RecipeJobStatus { + job_id: string + status: RecipeJobStatusValue + result: RecipeResult | null + error: string | null +} + export interface RecipeRequest { pantry_items: string[] secondary_pantry_items: Record @@ -593,6 +602,18 @@ export const recipesAPI = { const response = await api.post('/recipes/suggest', req, { timeout: 120000 }) return response.data }, + + /** Submit an async job for L3/L4 generation. Returns job_id + initial status. */ + async suggestAsync(req: RecipeRequest): Promise<{ job_id: string; status: string }> { + const response = await api.post('/recipes/suggest', req, { params: { async: 'true' }, timeout: 15000 }) + return response.data + }, + + /** Poll an async job. Returns the full status including result once done. */ + async pollJob(jobId: string): Promise { + const response = await api.get(`/recipes/jobs/${jobId}`, { timeout: 10000 }) + return response.data + }, async getRecipe(id: number): Promise { const response = await api.get(`/recipes/${id}`) return response.data diff --git a/frontend/src/stores/recipes.ts b/frontend/src/stores/recipes.ts index 4798e17..ffff6cf 100644 --- a/frontend/src/stores/recipes.ts +++ b/frontend/src/stores/recipes.ts @@ -7,7 +7,7 @@ import { defineStore } from 'pinia' import { ref, computed, watch } from 'vue' -import { recipesAPI, type RecipeResult, type RecipeSuggestion, type RecipeRequest, type NutritionFilters } from '../services/api' +import { recipesAPI, type RecipeResult, type RecipeSuggestion, type RecipeRequest, type RecipeJobStatusValue, type NutritionFilters } from '../services/api' const DISMISSED_KEY = 'kiwi:dismissed_recipes' const DISMISS_TTL_MS = 7 * 24 * 60 * 60 * 1000 @@ -121,6 +121,7 @@ export const useRecipesStore = defineStore('recipes', () => { const result = ref(null) const loading = ref(false) const error = ref(null) + const jobStatus = ref(null) // Request parameters const level = ref(1) @@ -199,18 +200,57 @@ export const useRecipesStore = defineStore('recipes', () => { async function suggest(pantryItems: string[], secondaryPantryItems: Record = {}) { loading.value = true error.value = null + jobStatus.value = null seenIds.value = new Set() try { - result.value = await recipesAPI.suggest(_buildRequest(pantryItems, secondaryPantryItems)) - _trackSeen(result.value.suggestions) + if (level.value >= 3) { + await _suggestAsync(pantryItems, secondaryPantryItems) + } else { + result.value = await recipesAPI.suggest(_buildRequest(pantryItems, secondaryPantryItems)) + _trackSeen(result.value.suggestions) + } } catch (err: unknown) { error.value = err instanceof Error ? err.message : 'Failed to get recipe suggestions' } finally { loading.value = false + jobStatus.value = null } } + async function _suggestAsync(pantryItems: string[], secondaryPantryItems: Record) { + const queued = await recipesAPI.suggestAsync(_buildRequest(pantryItems, secondaryPantryItems)) + + // CLOUD_MODE or future sync fallback: server returned result directly (status 200) + if ('suggestions' in queued) { + result.value = queued as unknown as RecipeResult + _trackSeen(result.value.suggestions) + return + } + + jobStatus.value = 'queued' + const { job_id } = queued + const deadline = Date.now() + 90_000 + const POLL_MS = 2_500 + + while (Date.now() < deadline) { + await new Promise((resolve) => setTimeout(resolve, POLL_MS)) + const poll = await recipesAPI.pollJob(job_id) + jobStatus.value = poll.status + + if (poll.status === 'done') { + result.value = poll.result + if (result.value) _trackSeen(result.value.suggestions) + return + } + if (poll.status === 'failed') { + throw new Error(poll.error ?? 'Recipe generation failed') + } + } + + throw new Error('Recipe generation timed out — the model may be busy. Try again.') + } + async function loadMore(pantryItems: string[], secondaryPantryItems: Record = {}) { if (!result.value || loading.value) return loading.value = true @@ -308,6 +348,7 @@ export const useRecipesStore = defineStore('recipes', () => { result, loading, error, + jobStatus, level, constraints, allergies,