Project Update
This commit is contained in:
28
Dockerfile
Normal file
28
Dockerfile
Normal file
@@ -0,0 +1,28 @@
|
||||
# Stage 1: Build the Nuxt 4 frontend (Debian Slim)
|
||||
FROM node:20-bookworm-slim AS frontend-build
|
||||
WORKDIR /src/frontend
|
||||
COPY Seasoned.Frontend/package*.json ./
|
||||
RUN npm install
|
||||
COPY Seasoned.Frontend/ .
|
||||
RUN npx nuxi generate
|
||||
|
||||
# Stage 2: Build the .NET 9 backend (Debian Slim)
|
||||
FROM mcr.microsoft.com/dotnet/sdk:9.0-bookworm-slim AS backend-build
|
||||
WORKDIR /src
|
||||
COPY Seasoned.Backend/*.csproj ./Seasoned.Backend/
|
||||
RUN dotnet restore ./Seasoned.Backend/Seasoned.Backend.csproj
|
||||
COPY . .
|
||||
|
||||
# Copy Nuxt static files into .NET wwwroot
|
||||
COPY --from=frontend-build /src/frontend/.output/public ./Seasoned.Backend/wwwroot
|
||||
|
||||
RUN dotnet publish ./Seasoned.Backend/Seasoned.Backend.csproj -c Release -o /app
|
||||
|
||||
# Stage 3: Final Runtime (Debian Slim)
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:9.0-bookworm-slim
|
||||
WORKDIR /app
|
||||
COPY --from=backend-build /app .
|
||||
|
||||
# Essential for PGVector and Gemini logic to run smoothly
|
||||
ENV ASPNETCORE_URLS=http://+:5000
|
||||
ENTRYPOINT ["dotnet", "Seasoned.Backend.dll"]
|
||||
@@ -5,12 +5,11 @@ using Seasoned.Backend.DTOs;
|
||||
namespace Seasoned.Backend.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
[Route("api/recipe")]
|
||||
public class RecipeController : ControllerBase
|
||||
{
|
||||
private readonly IRecipeService _recipeService;
|
||||
|
||||
// Dependency Injection: The service is "injected" here
|
||||
public RecipeController(IRecipeService recipeService)
|
||||
{
|
||||
_recipeService = recipeService;
|
||||
|
||||
17
Seasoned.Backend/Data/ApplicationDbContext.cs
Normal file
17
Seasoned.Backend/Data/ApplicationDbContext.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Seasoned.Backend.Models;
|
||||
|
||||
namespace Seasoned.Backend.Data;
|
||||
|
||||
public class ApplicationDbContext : DbContext
|
||||
{
|
||||
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
|
||||
: base(options) { }
|
||||
|
||||
public DbSet<Recipe> Recipes { get; set; }
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder.HasPostgresExtension("vector");
|
||||
}
|
||||
}
|
||||
64
Seasoned.Backend/Data/Migrations/20260305044721_SeasonedInit.Designer.cs
generated
Normal file
64
Seasoned.Backend/Data/Migrations/20260305044721_SeasonedInit.Designer.cs
generated
Normal file
@@ -0,0 +1,64 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
using Seasoned.Backend.Data;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Seasoned.Backend.Data.Migrations
|
||||
{
|
||||
[DbContext(typeof(ApplicationDbContext))]
|
||||
[Migration("20260305044721_SeasonedInit")]
|
||||
partial class SeasonedInit
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "9.0.2")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "vector");
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("Seasoned.Backend.Models.Recipe", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.PrimitiveCollection<List<string>>("Ingredients")
|
||||
.IsRequired()
|
||||
.HasColumnType("text[]");
|
||||
|
||||
b.PrimitiveCollection<List<string>>("Instructions")
|
||||
.IsRequired()
|
||||
.HasColumnType("text[]");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Recipes");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Seasoned.Backend.Data.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class SeasonedInit : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterDatabase()
|
||||
.Annotation("Npgsql:PostgresExtension:vector", ",,");
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Recipes",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
Title = table.Column<string>(type: "text", nullable: false),
|
||||
Description = table.Column<string>(type: "text", nullable: false),
|
||||
Ingredients = table.Column<List<string>>(type: "text[]", nullable: false),
|
||||
Instructions = table.Column<List<string>>(type: "text[]", nullable: false),
|
||||
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Recipes", x => x.Id);
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "Recipes");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
using Seasoned.Backend.Data;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Seasoned.Backend.Data.Migrations
|
||||
{
|
||||
[DbContext(typeof(ApplicationDbContext))]
|
||||
partial class ApplicationDbContextModelSnapshot : ModelSnapshot
|
||||
{
|
||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "9.0.2")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "vector");
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("Seasoned.Backend.Models.Recipe", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.PrimitiveCollection<List<string>>("Ingredients")
|
||||
.IsRequired()
|
||||
.HasColumnType("text[]");
|
||||
|
||||
b.PrimitiveCollection<List<string>>("Instructions")
|
||||
.IsRequired()
|
||||
.HasColumnType("text[]");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Recipes");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Seasoned.Backend/Models/Recipe.cs
Normal file
11
Seasoned.Backend/Models/Recipe.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
namespace Seasoned.Backend.Models;
|
||||
|
||||
public class Recipe
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Title { get; set; } = string.Empty;
|
||||
public string Description { get; set; } = string.Empty;
|
||||
public List<string> Ingredients { get; set; } = new();
|
||||
public List<string> Instructions { get; set; } = new();
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
}
|
||||
@@ -1,10 +1,18 @@
|
||||
using Seasoned.Backend.Services;
|
||||
using Microsoft.AspNetCore.HttpOverrides;
|
||||
using System.Text.Json;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Seasoned.Backend.Data;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
builder.Services.AddScoped<IRecipeService, RecipeService>();
|
||||
|
||||
builder.Services.AddControllers();
|
||||
builder.Services.AddControllers()
|
||||
.AddJsonOptions(options => {
|
||||
options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
|
||||
});
|
||||
|
||||
builder.Services.AddOpenApi();
|
||||
|
||||
builder.Services.AddCors(options =>
|
||||
@@ -17,7 +25,20 @@ builder.Services.AddCors(options =>
|
||||
});
|
||||
});
|
||||
|
||||
builder.Services.AddDbContext<ApplicationDbContext>(options =>
|
||||
options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection"),
|
||||
o => o.UseVector()));
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
using (var scope = app.Services.CreateScope())
|
||||
{
|
||||
var db = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
|
||||
db.Database.EnsureCreated();
|
||||
}
|
||||
|
||||
app.UseDefaultFiles();
|
||||
app.UseStaticFiles();
|
||||
app.UseCors("AllowAll");
|
||||
|
||||
if (app.Environment.IsDevelopment())
|
||||
@@ -26,4 +47,5 @@ if (app.Environment.IsDevelopment())
|
||||
}
|
||||
|
||||
app.MapControllers();
|
||||
app.MapFallbackToFile("index.html");
|
||||
app.Run();
|
||||
@@ -10,6 +10,13 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.13" />
|
||||
<PackageReference Include="Mscc.GenerativeAI" Version="2.2.8" />
|
||||
<PackageReference Include="Pgvector.EntityFrameworkCore" Version="0.3.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.2">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.2" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.2" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -2,7 +2,8 @@ using Seasoned.Backend.DTOs;
|
||||
using Mscc.GenerativeAI;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using System.IO;
|
||||
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
namespace Seasoned.Backend.Services;
|
||||
|
||||
public class RecipeService : IRecipeService
|
||||
@@ -11,7 +12,7 @@ public class RecipeService : IRecipeService
|
||||
|
||||
public RecipeService(IConfiguration config)
|
||||
{
|
||||
_apiKey = config["GeminiApiKey"] ?? throw new ArgumentNullException("API Key missing");
|
||||
_apiKey = config["GEMINI_API_KEY"] ?? throw new ArgumentNullException("API Key missing");
|
||||
}
|
||||
|
||||
public async Task<RecipeResponseDto> ParseRecipeImageAsync(IFormFile image)
|
||||
@@ -23,27 +24,55 @@ public class RecipeService : IRecipeService
|
||||
await image.CopyToAsync(ms);
|
||||
var base64Image = Convert.ToBase64String(ms.ToArray());
|
||||
|
||||
// 1. Better Prompt: Tell Gemini exactly what the JSON should look like
|
||||
var prompt = @"Extract the recipe from this image.
|
||||
Return a JSON object with exactly these fields:
|
||||
var prompt = @"Extract the recipe details from this image.
|
||||
IMPORTANT: Return ONLY a raw JSON string.
|
||||
DO NOT include markdown formatting (no ```json).
|
||||
DO NOT include any text before or after the JSON.
|
||||
All property names and string values MUST be enclosed in double quotes.
|
||||
JSON structure:
|
||||
{
|
||||
""title"": ""string"",
|
||||
""description"": ""string"",
|
||||
""ingredients"": [""string""],
|
||||
""instructions"": [""string""]
|
||||
""ingredients"": [""string"", ""string""],
|
||||
""instructions"": [""string"", ""string""]
|
||||
}";
|
||||
|
||||
// 2. Set the Response MIME Type to application/json
|
||||
var config = new GenerationConfig { ResponseMimeType = "application/json" };
|
||||
var config = new GenerationConfig {
|
||||
ResponseMimeType = "application/json",
|
||||
Temperature = 0.1f
|
||||
};
|
||||
|
||||
var request = new GenerateContentRequest(prompt, config);
|
||||
request.AddMedia(base64Image, "image/png");
|
||||
await Task.Run(() => request.AddMedia(base64Image, "image/png"));
|
||||
|
||||
var response = await model.GenerateContent(request);
|
||||
string rawText = response.Text?.Trim() ?? "";
|
||||
|
||||
// 3. Use System.Text.Json to turn that string back into our DTO
|
||||
var options = new System.Text.Json.JsonSerializerOptions { PropertyNameCaseInsensitive = true };
|
||||
var result = System.Text.Json.JsonSerializer.Deserialize<RecipeResponseDto>(response.Text, options);
|
||||
int start = rawText.IndexOf('{');
|
||||
int end = rawText.LastIndexOf('}');
|
||||
|
||||
return result ?? new RecipeResponseDto { Title = "Error parsing JSON" };
|
||||
if (start == -1 || end == -1)
|
||||
{
|
||||
return new RecipeResponseDto { Title = "Error", Description = "AI failed to generate a valid JSON block." };
|
||||
}
|
||||
|
||||
string cleanJson = rawText.Substring(start, (end - start) + 1);
|
||||
|
||||
try
|
||||
{
|
||||
var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
|
||||
var result = JsonSerializer.Deserialize<RecipeResponseDto>(cleanJson, options);
|
||||
return result ?? new RecipeResponseDto { Title = "Empty Response" };
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
Console.WriteLine($"Raw AI Output: {rawText}");
|
||||
Console.WriteLine($"Failed to parse JSON: {ex.Message}");
|
||||
|
||||
return new RecipeResponseDto {
|
||||
Title = "Parsing Error",
|
||||
Description = "The AI response was malformed. Check logs."
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
116
Seasoned.Frontend/app/assets/css/app-theme.css
Normal file
116
Seasoned.Frontend/app/assets/css/app-theme.css
Normal 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;
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
30
Seasoned.sln
Normal file
30
Seasoned.sln
Normal file
@@ -0,0 +1,30 @@
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.5.2.0
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Seasoned.Tests", "Seasoned.Tests\Seasoned.Tests.csproj", "{FEF19240-CC2D-9BD4-7AC8-277372525020}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Seasoned.Backend", "Seasoned.Backend\Seasoned.Backend.csproj", "{874CAB7B-A43B-C80D-494C-C243C69AEE7A}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{FEF19240-CC2D-9BD4-7AC8-277372525020}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{FEF19240-CC2D-9BD4-7AC8-277372525020}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{FEF19240-CC2D-9BD4-7AC8-277372525020}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{FEF19240-CC2D-9BD4-7AC8-277372525020}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{874CAB7B-A43B-C80D-494C-C243C69AEE7A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{874CAB7B-A43B-C80D-494C-C243C69AEE7A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{874CAB7B-A43B-C80D-494C-C243C69AEE7A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{874CAB7B-A43B-C80D-494C-C243C69AEE7A}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {ED9A436F-5793-4685-B738-3AA5653D4AF1}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
34
compose.yaml
Normal file
34
compose.yaml
Normal file
@@ -0,0 +1,34 @@
|
||||
services:
|
||||
# Database: Postgres with pgvector
|
||||
db:
|
||||
image: pgvector/pgvector:pg16
|
||||
container_name: seasoned-db
|
||||
restart: always
|
||||
environment:
|
||||
- POSTGRES_USER=${DB_USER}
|
||||
- POSTGRES_PASSWORD=${DB_PASSWORD}
|
||||
- POSTGRES_DB=${DB_NAME}
|
||||
ports:
|
||||
- "5432:5432"
|
||||
volumes:
|
||||
- pgdata:/var/lib/postgresql/data
|
||||
- ./init-db:/docker-entrypoint-initdb.d
|
||||
|
||||
# Seasoned App: C# .NET 9 + Nuxt 4
|
||||
app:
|
||||
build: .
|
||||
container_name: seasoned-main
|
||||
restart: always
|
||||
ports:
|
||||
- "5000:8080"
|
||||
environment:
|
||||
- GEMINI_API_KEY=${GEMINI_API_KEY}
|
||||
- NUXT_PUBLIC_API_BASE=${NUXT_PUBLIC_API_BASE}
|
||||
- ConnectionStrings__DefaultConnection=Host=db;Database=${DB_NAME};Username=${DB_USER};Password=${DB_PASSWORD}
|
||||
- ASPNETCORE_ENVIRONMENT=Production
|
||||
- ASPNETCORE_HTTP_PORTS=8080
|
||||
depends_on:
|
||||
- db
|
||||
|
||||
volumes:
|
||||
pgdata:
|
||||
Reference in New Issue
Block a user