Project Update

This commit is contained in:
2026-03-05 04:52:15 +00:00
parent a99a477985
commit 2bbbbc746f
15 changed files with 551 additions and 93 deletions

View File

@@ -1,62 +1,71 @@
<template>
<v-app>
<v-app class="recipe-bg">
<v-main>
<v-container>
<v-card class="pa-5 mx-auto mt-10" max-width="500" elevation="10">
<v-card-title class="text-center">Seasoned AI</v-card-title>
<v-card class="recipe-card pa-10 mx-auto mt-10" max-width="950" elevation="1">
<v-divider class="my-3"></v-divider>
<header class="text-center mb-10">
<h1 class="brand-title">Seasoned</h1>
<p class="brand-subtitle">A Recipe Collection</p>
</header>
<v-divider class="mb-10 separator"></v-divider>
<v-file-input
v-model="files"
label="Pick a recipe photo"
prepend-icon="mdi-camera"
variant="outlined"
accept="image/*"
></v-file-input>
<v-row justify="center" class="mb-12">
<v-col cols="12" md="8">
<v-file-input
v-model="files"
label="Upload Image"
variant="solo-filled"
flat
accept="image/*"
class="custom-input mb-4"
></v-file-input>
<v-btn
color="primary"
block
size="x-large"
:loading="loading"
@click="uploadImage"
>
Analyze Recipe
</v-btn>
<v-btn
class="analyze-btn"
block
size="x-large"
elevation="0"
:loading="loading"
@click="uploadImage"
>
Analyze Recipe
</v-btn>
</v-col>
</v-row>
<div v-if="recipe" class="mt-5">
<h2 class="text-h4 mb-4">{{ recipe.title }}</h2>
<p class="text-subtitle-1 mb-6 text-grey-darken-1">{{ recipe.description }}</p>
<transition name="fade">
<div v-if="recipe" class="recipe-content">
<h2 class="recipe-title text-center mb-4">{{ recipe.title }}</h2>
<p class="recipe-description text-center mb-12 text-italic">{{ recipe.description }}</p>
<v-row>
<v-col cols="12" md="5">
<h3 class="text-h6 mb-2">Ingredients</h3>
<v-list lines="one" variant="flat" class="bg-grey-lighten-4 rounded-lg">
<v-list-item v-for="(item, i) in recipe.ingredients" :key="i">
<template v-slot:prepend>
<v-icon icon="mdi-circle-small"></v-icon>
</template>
{{ item }}
</v-list-item>
</v-list>
</v-col>
<v-row>
<v-col cols="12" md="5">
<div class="section-header mb-6 px-2">
<v-icon icon="mdi-spoon-sugar" class="mr-2" size="small"></v-icon>
<span>Ingredients</span>
</div>
<v-list class="ingredients-list">
<v-list-item v-for="(item, i) in recipe.ingredients" :key="i" class="ingredient-item">
{{ item }}
</v-list-item>
</v-list>
</v-col>
<v-col cols="12" md="7">
<h3 class="text-h6 mb-2">Instructions</h3>
<v-timeline side="end" align="start" density="compact">
<v-timeline-item
v-for="(step, i) in recipe.instructions"
:key="i"
dot-color="primary"
size="x-small"
>
<div class="text-body-1">{{ step }}</div>
</v-timeline-item>
</v-timeline>
</v-col>
</v-row>
</div>
<v-col cols="12" md="7">
<div class="section-header mb-6 px-2">
<v-icon icon="mdi-pot-steam-outline" class="mr-2" size="small"></v-icon>
<span>Instructions</span>
</div>
<div v-for="(step, i) in recipe.instructions" :key="i" class="instruction-step mb-8">
<span class="step-number">{{ i + 1 }}.</span>
<p class="step-text">{{ step }}</p>
</div>
</v-col>
</v-row>
</div>
</transition>
</v-card>
</v-container>
</v-main>
@@ -66,43 +75,28 @@
<script setup>
import axios from 'axios'
import { ref } from 'vue'
import '@/assets/css/app-theme.css'
const config = useRuntimeConfig()
const files = ref([])
const loading = ref(false)
const recipe = ref(null)
const uploadImage = async () => {
// 1. Debug: Check what Vuetify is actually giving us
console.log("Files variable:", files.value);
// Vuetify 3 v-file-input can return a single File or an Array of Files
// We need to ensure we have the actual File object
const fileToUpload = Array.isArray(files.value) ? files.value[0] : files.value;
if (!fileToUpload) {
alert("Please select a file first!");
return;
}
if (!fileToUpload) return;
loading.value = true;
const formData = new FormData();
// 2. Append the file. The string 'image' MUST match your C# parameter name
formData.append('image', fileToUpload);
try {
// 3. Post with explicit multipart/form-data header (Axios usually does this, but let's be sure)
const response = await axios.post('http://localhost:5000/api/recipe/upload', formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
const response = await $fetch(`${config.public.apiBase}api/recipe/upload`, {
method: 'POST',
body: formData
});
recipe.value = response.data;
console.log("Success:", response.data);
recipe.value = response;
} catch (error) {
console.error("Detailed Error:", error.response?.data || error.message);
alert("Backend error: Check the browser console for details.");
console.error("Error:", error);
} finally {
loading.value = false;
}

View File

@@ -0,0 +1,116 @@
@import url('https://fonts.googleapis.com/css2?family=Libre+Baskerville:ital,wght@0,400;0,700;1,400&family=Inter:wght@400;600&display=swap');
.recipe-bg {
background-color: #3e2a14 !important;
background-image: url("https://www.transparenttextures.com/patterns/dark-wood.png") !important; /* Richer wood texture */
background-size: cover;
}
.recipe-card {
background-color: #f4e4bc !important;
background-image: url("https://www.transparenttextures.com/patterns/natural-paper.png"); /* Stronger linen texture */
border: 1px solid #c9b996 !important;
border-radius: 12px !important;
font-family: 'Inter', sans-serif;
}
.brand-title {
font-family: 'Libre Baskerville', serif;
font-weight: 700;
font-size: 2.8rem;
color: #2e1e0a;
letter-spacing: -1px;
}
.brand-subtitle {
font-family: 'Libre Baskerville', serif;
font-style: italic;
color: #6d5e4a;
font-size: 0.85rem;
text-transform: uppercase;
letter-spacing: 3px;
}
.recipe-title {
font-family: 'Libre Baskerville', serif;
font-size: 2.4rem;
color: #1e1408;
}
.section-header {
font-family: 'Libre Baskerville', serif;
font-weight: bold;
font-size: 1.1rem;
border-bottom: 2px solid #dccca7;
color: #4a3a2a;
display: flex;
align-items: center;
text-transform: uppercase;
}
.custom-input .v-field {
background-color: #5d4037 !important;
color: #f8f1e0 !important;
border-radius: 8px !important;
border: 2px solid #5d4037 !important;
}
.custom-input .v-label {
color: #f8f1e0 !important;
opacity: 1 !important;
font-weight: 600;
}
.custom-input .v-icon {
color: #f8f1e0 !important;
}
.custom-input .v-field--focused {
border: 2px solid #556b2f !important;
}
.analyze-btn {
background-color: #556b2f !important;
color: #ffffff !important;
font-family: 'Libre Baskerville', serif;
text-transform: none;
font-size: 1.1rem;
letter-spacing: 0.5px;
}
.ingredients-list {
background: transparent !important;
}
.ingredient-item {
border-bottom: 1px dotted #c1b18e;
font-style: italic;
color: #2c2925;
}
.instruction-step {
display: flex;
gap: 20px;
line-height: 1.6;
color: #2c2925;
}
.step-number {
font-family: 'Libre Baskerville', serif;
font-weight: bold;
color: #3b4e1e;
font-size: 1.3rem;
min-width: 25px;
}
.separator {
border-color: #dccca7 !important;
opacity: 1;
}
.fade-enter-active, .fade-leave-active {
transition: opacity 0.6s ease;
}
.fade-enter-from, .fade-leave-to {
opacity: 0;
}

View File

@@ -23,16 +23,18 @@ export default defineNuxtConfig({
],
runtimeConfig: {
geminiApiKey: '',
public: {
apiBase: ''
}
},
vite: {
server: {
hmr: {
hmr: process.env.NODE_ENV !== 'production' ? {
protocol: 'ws',
host: 'localhost',
port: 3000
}
} : undefined
}
}
})