Restore code and save recent updates

This commit is contained in:
2026-01-25 03:31:01 +00:00
parent bae861c71f
commit 5ce0b4d278
54 changed files with 2963 additions and 2899 deletions

View File

@@ -30,6 +30,7 @@
"@sveltejs/kit": "^2.50.1",
"@sveltejs/vite-plugin-svelte": "^5.1.1",
"@tauri-apps/cli": "^2.9.6",
"@types/node": "^25.0.10",
"svelte": "^5.48.2",
"svelte-check": "^4.3.5",
"typescript": "~5.6.3",

View File

@@ -9,14 +9,14 @@ const __dirname = path.dirname(__filename);
const app = express();
const PORT = process.env.PORT || 3000;
// Enable gzip compression
app.use(compression());
// Serve static files from the build directory (one level up from server folder)
const buildPath = path.join(__dirname, '../build');
app.use(express.static(buildPath));
// Handle SPA routing: serve index.html for any unknown routes
app.get(/.*/, (req, res) => {
res.sendFile(path.join(buildPath, 'index.html'));
});

View File

@@ -1,4 +1,4 @@
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
#[tauri::command]
fn greet(name: &str) -> String {
format!("Hello, {}! You've been greeted from Rust!", name)

View File

@@ -1,4 +1,4 @@
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
fn main() {

View File

@@ -201,7 +201,7 @@
const config = activeConfig();
if (!config.staticScene || config.scenes) {
// Always use window scroll now
scrollContainer = null;
updateMeasurements();
}

View File

@@ -1,5 +1,6 @@
<script lang="ts">
import Icon from "@iconify/svelte";
import Pagination from "./Pagination.svelte";
let {
viewMode = $bindable(),
@@ -7,24 +8,32 @@
selectedCategory = $bindable(),
categories,
onSearchInput,
currentPage,
totalPages,
goToPage,
}: {
viewMode: "company" | "user";
searchQuery: string;
selectedCategory: string;
categories: string[];
onSearchInput: () => void;
currentPage: number;
totalPages: number;
goToPage: (page: number) => void;
} = $props();
</script>
<div
class="bg-black/80 backdrop-blur-[30px] border border-white/20 rounded-[32px] p-10 mb-10 shadow-[0_32px_64px_rgba(0,0,0,0.4)]"
class="bg-black/60 backdrop-blur-2xl border border-white/15 rounded-4xl p-12 lg:p-14 mb-12 shadow-2xl shadow-black/50"
>
<!-- Header content -->
<div class="mb-8 px-3 text-center">
<h1 class="text-white text-[42px] font-black m-0 tracking-[-2px]">
<div class="mb-10 px-4 text-center">
<h1
class="text-white text-[42px] lg:text-[48px] font-black m-0 tracking-[-2px]"
>
Sustainability Database
</h1>
<p class="text-white/70 text-base mt-2 font-medium">
<p class="text-white/80 text-base lg:text-lg mt-3 font-medium">
{#if viewMode === "company"}
Search within verified company reports and impact assessments
{:else}
@@ -33,35 +42,35 @@
</p>
</div>
<!-- View Mode Toggle Switch -->
<div class="flex items-center justify-center gap-4 my-6">
<div class="flex items-center justify-center gap-5 my-8">
<span
class="flex items-center gap-1.5 text-sm font-semibold transition-all duration-300 {viewMode ===
class="flex items-center gap-2 text-sm lg:text-base font-semibold transition-all duration-300 {viewMode ===
'company'
? 'text-emerald-400'
: 'text-white/40'}"
: 'text-white/50'}"
>
<Icon icon="ri:building-2-line" width="16" />
Company
</span>
<button
class="relative w-14 h-7 bg-white/15 border-none rounded-full cursor-pointer transition-all duration-300 p-0 hover:bg-white/20"
class="relative w-16 h-8 bg-white/10 border-none rounded-full cursor-pointer transition-all duration-300 p-0 hover:bg-white/15 shadow-inner"
onclick={() =>
(viewMode = viewMode === "company" ? "user" : "company")}
aria-label="Toggle between company and user reports"
>
<span
class="absolute top-1/2 -translate-y-1/2 w-[22px] h-[22px] bg-emerald-600 rounded-full transition-all duration-300 shadow-[0_2px_8px_rgba(34,197,94,0.4)] {viewMode ===
class="absolute top-1/2 -translate-y-1/2 w-6 h-6 bg-emerald-500 rounded-full transition-all duration-300 shadow-[0_2px_12px_rgba(34,197,94,0.5)] {viewMode ===
'user'
? 'left-[calc(100%-25px)]'
: 'left-[3px]'}"
? 'left-[calc(100%-28px)]'
: 'left-1'}"
></span>
</button>
<span
class="flex items-center gap-1.5 text-sm font-semibold transition-all duration-300 {viewMode ===
class="flex items-center gap-2 text-sm lg:text-base font-semibold transition-all duration-300 {viewMode ===
'user'
? 'text-emerald-400'
: 'text-white/40'}"
: 'text-white/50'}"
>
<Icon icon="ri:user-voice-line" width="16" />
User Reports
@@ -69,33 +78,35 @@
</div>
{#if viewMode === "company"}
<div class="relative max-w-[600px] mx-auto mb-8">
<div class="relative max-w-[40.625rem] mx-auto mb-10">
<div
class="absolute left-5 top-1/2 -translate-y-1/2 text-white/60 flex items-center pointer-events-none"
class="absolute left-6 top-1/2 -translate-y-1/2 text-white/60 flex items-center pointer-events-none"
>
<Icon icon="ri:search-line" width="20" />
<Icon icon="ri:search-line" width="22" />
</div>
<input
type="text"
class="w-full bg-white/5 border border-white/10 rounded-full py-4 pl-[52px] pr-5 text-white text-base font-medium outline-none transition-all duration-200 focus:bg-white/10 focus:border-emerald-400 placeholder:text-white/40"
class="w-full bg-black/30 border border-white/15 rounded-full py-4 lg:py-5 pl-14 pr-6 text-white text-base lg:text-lg font-medium outline-none transition-all duration-200 focus:bg-black/40 focus:border-emerald-400 placeholder:text-white/50 shadow-inner"
placeholder="Search for companies, topics (e.g., 'emissions')..."
bind:value={searchQuery}
oninput={onSearchInput}
/>
</div>
<div class="flex justify-center flex-wrap gap-3 mb-5">
<div class="flex justify-center flex-wrap gap-3.5 mb-6">
{#each categories as category}
<button
class="px-6 py-2.5 rounded-full text-sm font-semibold cursor-pointer transition-all duration-200 border {selectedCategory ===
class="px-7 py-3 rounded-full text-sm lg:text-base font-semibold cursor-pointer transition-all duration-200 border {selectedCategory ===
category
? 'bg-emerald-500 border-emerald-500 text-emerald-950 shadow-[0_4px_15px_rgba(34,197,94,0.3)]'
: 'bg-white/5 border-white/10 text-white/70 hover:bg-white/10 hover:text-white hover:-translate-y-0.5'}"
? 'bg-emerald-500 border-emerald-500 text-emerald-950 shadow-[0_4px_20px_rgba(34,197,94,0.4)]'
: 'bg-black/30 backdrop-blur-sm border-white/15 text-white/70 hover:bg-black/40 hover:text-white hover:-translate-y-0.5 hover:shadow-lg'}"
onclick={() => (selectedCategory = category)}
>
{category}
</button>
{/each}
</div>
<Pagination {currentPage} {totalPages} {goToPage} />
{/if}
</div>

View File

@@ -25,12 +25,13 @@
aria-label="Close modal"
>
<div
class="bg-slate-900 border border-slate-700 rounded-[24px] w-full max-w-[1000px] h-[90vh] flex flex-col shadow-[0_24px_64px_rgba(0,0,0,0.5)] overflow-hidden outline-none"
class="bg-slate-900 border border-slate-700 rounded-3xl w-full max-w-5xl h-[90vh] flex flex-col shadow-[0_24px_64px_rgba(0,0,0,0.5)] overflow-hidden outline-none"
onclick={(e) => e.stopPropagation()}
onkeydown={(e) => e.stopPropagation()}
transition:scale={{ duration: 300, start: 0.95 }}
role="document"
tabindex="0"
role="dialog"
aria-modal="true"
tabindex="-1"
>
<div
class="px-6 py-5 bg-slate-800 border-b border-slate-700 flex justify-between items-center shrink-0"

View File

@@ -7,6 +7,7 @@
detected_brand: string;
user_description?: string;
created_at: string;
image_base64?: string;
analysis: {
verdict: string;
confidence: string;
@@ -21,66 +22,82 @@
</script>
<button
class="flex items-center gap-6 p-6 bg-black/80 backdrop-blur-[30px] border border-red-500/30 rounded-[24px] transition-all duration-300 shadow-[0_16px_48px_rgba(0,0,0,0.5)] w-full text-left cursor-pointer hover:bg-black/95 hover:border-red-500/60 hover:-translate-y-1 hover:scale-[1.01] hover:shadow-[0_24px_64px_rgba(0,0,0,0.6)] outline-none"
class="flex items-center gap-6 lg:gap-8 p-7 lg:p-8 bg-black/60 backdrop-blur-2xl border border-red-500/30 rounded-3xl transition-all duration-300 shadow-2xl shadow-black/50 w-full text-left cursor-pointer hover:bg-black/70 hover:border-red-500/60 hover:-translate-y-1 hover:scale-[1.01] hover:shadow-[0_24px_72px_rgba(0,0,0,0.7)] outline-none group"
{onclick}
>
<div
class="w-[52px] h-[52px] rounded-xe flex items-center justify-center shrink-0 rounded-[14px]
{incident.analysis?.severity === 'high'
? 'bg-red-500/20 text-red-500'
: 'bg-amber-500/20 text-amber-500'}"
>
<Icon icon="ri:alert-fill" width="28" />
</div>
{#if incident.image_base64}
<div
class="w-16 h-16 lg:w-20 lg:h-20 shrink-0 rounded-2xl overflow-hidden border border-white/10 group-hover:border-white/20 transition-colors"
>
<img
src="data:image/jpeg;base64,{incident.image_base64}"
alt={incident.product_name}
class="w-full h-full object-cover"
/>
</div>
{:else}
<div
class="w-14 h-14 lg:w-16 lg:h-16 rounded-xe flex items-center justify-center shrink-0 rounded-2xl transition-transform
{incident.analysis?.severity === 'high'
? 'bg-red-500/20 text-red-500'
: 'bg-amber-500/20 text-amber-500'}"
>
<Icon icon="ri:alert-fill" width="32" class="lg:w-9" />
</div>
{/if}
<div class="flex-1 min-w-0">
<div class="flex items-center gap-3 mb-1.5">
<h3 class="text-white text-[18px] font-bold m-0 italic">
<div class="flex items-center gap-3 mb-2">
<h3
class="text-white text-[19px] lg:text-[20px] font-bold m-0 italic"
>
{incident.product_name}
</h3>
{#if incident.detected_brand && incident.detected_brand !== "Unknown"}
<span
class="bg-white/10 text-white/70 px-2.5 py-0.5 rounded-full text-[12px] font-semibold"
class="bg-white/10 backdrop-blur-sm text-white/80 px-3 py-1 rounded-full text-[12px] lg:text-[13px] font-semibold"
>{incident.detected_brand}</span
>
{/if}
</div>
<p class="text-white/70 text-sm m-0 mb-2.5 leading-tight italic">
<p
class="text-white/70 text-sm lg:text-base m-0 mb-3 leading-tight italic"
>
{incident.analysis?.verdict || "Greenwashing detected"}
</p>
<div class="flex flex-wrap gap-2">
<div class="flex flex-wrap gap-2.5">
<span
class="flex items-center gap-1 px-2.5 py-1 rounded-full text-[11px] font-semibold capitalize
class="flex items-center gap-1.5 px-3 py-1.5 rounded-full text-[11px] lg:text-[12px] font-semibold capitalize backdrop-blur-sm
{incident.analysis?.severity === 'high'
? 'bg-red-500/20 text-red-400'
: incident.analysis?.severity === 'medium'
? 'bg-amber-500/20 text-amber-400'
: 'bg-emerald-500/20 text-emerald-400'}"
>
<Icon icon="ri:error-warning-fill" width="14" />
<Icon icon="ri:error-warning-fill" width="15" />
{incident.analysis?.severity || "unknown"} severity
</span>
<span
class="flex items-center gap-1 px-2.5 py-1 rounded-full text-[11px] font-semibold bg-indigo-500/20 text-indigo-300"
class="flex items-center gap-1.5 px-3 py-1.5 rounded-full text-[11px] lg:text-[12px] font-semibold bg-indigo-500/20 text-indigo-300 backdrop-blur-sm"
>
<Icon icon="ri:shield-check-fill" width="14" />
<Icon icon="ri:shield-check-fill" width="15" />
{incident.analysis?.confidence || "unknown"} confidence
</span>
<span
class="flex items-center gap-1 px-2.5 py-1 rounded-full text-[11px] font-semibold bg-white/10 text-white/60"
class="flex items-center gap-1.5 px-3 py-1.5 rounded-full text-[11px] lg:text-[12px] font-semibold bg-white/10 text-white/70 backdrop-blur-sm"
>
<Icon icon="ri:calendar-line" width="14" />
<Icon icon="ri:calendar-line" width="15" />
{new Date(incident.created_at).toLocaleDateString()}
</span>
</div>
</div>
<div
class="flex flex-col items-center gap-1 text-[11px] font-bold uppercase text-red-500"
class="flex flex-col items-center gap-1.5 text-[11px] lg:text-[12px] font-bold uppercase text-red-400"
>
<Icon icon="ri:spam-2-fill" width="24" class="text-red-500" />
<Icon icon="ri:spam-2-fill" width="26" class="text-red-400 lg:w-7" />
<span>Confirmed</span>
</div>
</button>

View File

@@ -8,6 +8,7 @@
detected_brand: string;
user_description?: string;
created_at: string;
image_base64?: string;
analysis: {
verdict: string;
confidence: string;
@@ -25,7 +26,7 @@
</script>
<div
class="fixed inset-0 bg-black/60 backdrop-blur-md z-1000 flex justify-center items-center p-5 outline-none"
class="fixed inset-0 bg-black/50 backdrop-blur-lg z-1000 flex justify-center items-center p-6 outline-none"
onclick={onclose}
onkeydown={(e) => e.key === "Escape" && onclose()}
transition:fade={{ duration: 200 }}
@@ -33,8 +34,10 @@
tabindex="0"
aria-label="Close modal"
>
<div
class="max-w-[700px] max-h-[85vh] w-full overflow-y-auto bg-black/85 backdrop-blur-[40px] border border-white/20 rounded-[32px] flex flex-col shadow-[0_32px_128px_rgba(0,0,0,0.7)] scrollbar-hide outline-none"
class="max-w-3xl max-h-[88vh] w-full overflow-y-auto bg-white/8 backdrop-blur-2xl border border-white/20 rounded-4xl flex flex-col shadow-2xl shadow-black/60 scrollbar-hide outline-none"
onclick={(e) => e.stopPropagation()}
onkeydown={(e) => e.stopPropagation()}
transition:scale={{ duration: 300, start: 0.95 }}
@@ -42,105 +45,138 @@
tabindex="0"
>
<div
class="px-10 py-[30px] bg-white/3 border-b border-red-500/20 flex justify-between items-center shrink-0"
class="px-10 lg:px-12 py-8 bg-white/5 border-b border-red-500/25 flex justify-between items-center shrink-0"
>
<div class="flex flex-col">
<div class="flex flex-col gap-2">
<div class="flex items-center gap-3">
<Icon
icon="ri:alert-fill"
width="28"
class="text-red-500"
width="30"
class="text-red-400"
/>
<h2 class="m-0 text-white text-[28px] font-extrabold">
<h2
class="m-0 text-white text-[28px] lg:text-[32px] font-extrabold"
>
{incident.product_name}
</h2>
</div>
{#if incident.detected_brand && incident.detected_brand !== "Unknown"}
<span class="mt-1 text-white/50 text-sm font-medium"
<span class="text-white/60 text-sm lg:text-base font-medium"
>Brand: {incident.detected_brand}</span
>
{/if}
</div>
<button
class="bg-white/5 border border-white/10 text-white w-10 h-10 rounded-xl flex items-center justify-center cursor-pointer transition-all duration-200 hover:bg-white/10 hover:rotate-90"
class="bg-white/5 border border-white/10 text-white w-11 h-11 rounded-xl flex items-center justify-center cursor-pointer transition-all duration-200 hover:bg-white/15 hover:rotate-90 hover:border-white/20"
onclick={onclose}
>
<Icon icon="ri:close-line" width="24" />
<Icon icon="ri:close-line" width="28" />
</button>
</div>
<div class="p-10 flex flex-col gap-[30px]">
<!-- Status Badges -->
<div class="flex flex-wrap gap-3">
<div class="p-10 lg:p-12 flex flex-col gap-8">
{#if incident.image_base64}
<div
class="w-full bg-black/20 rounded-3xl overflow-hidden border border-white/10 relative group"
>
<img
src="data:image/jpeg;base64,{incident.image_base64}"
alt="Evidence"
class="w-full max-h-[400px] object-contain bg-black/40"
/>
<div
class="absolute top-4 left-4 bg-black/60 backdrop-blur-md px-3 py-1.5 rounded-full flex items-center gap-2 border border-white/10"
>
<Icon
icon="ri:camera-fill"
width="16"
class="text-white/80"
/>
<span
class="text-xs font-bold text-white uppercase tracking-wider"
>Evidence of Greenwashing</span
>
</div>
</div>
{/if}
<div class="flex flex-wrap gap-3.5">
<span
class="flex items-center gap-2 px-5 py-3 rounded-[14px] text-[11px] font-extrabold tracking-wider
class="flex items-center gap-2 px-5 py-3 rounded-[14px] text-[11px] lg:text-[12px] font-extrabold tracking-wider backdrop-blur-sm
{incident.analysis?.severity === 'high'
? 'bg-red-500/20 text-red-400 border border-red-500/30'
? 'bg-red-500/25 text-red-300 border border-red-500/40'
: incident.analysis?.severity === 'medium'
? 'bg-amber-500/20 text-amber-400 border border-amber-500/30'
: 'bg-emerald-500/20 text-emerald-400 border border-emerald-500/30'} uppercase"
? 'bg-amber-500/25 text-amber-300 border border-amber-500/40'
: 'bg-emerald-500/25 text-emerald-300 border border-emerald-500/40'} uppercase"
>
<Icon icon="ri:error-warning-fill" width="18" />
{incident.analysis?.severity || "UNKNOWN"} SEVERITY
</span>
<span
class="flex items-center gap-2 px-5 py-3 rounded-[14px] text-[11px] font-extrabold tracking-wider bg-indigo-500/20 text-indigo-300 border border-indigo-500/30 uppercase"
class="flex items-center gap-2 px-5 py-3 rounded-[14px] text-[11px] lg:text-[12px] font-extrabold tracking-wider bg-indigo-500/25 text-indigo-300 border border-indigo-500/40 uppercase backdrop-blur-sm"
>
<Icon icon="ri:shield-check-fill" width="18" />
{incident.analysis?.confidence || "UNKNOWN"} CONFIDENCE
</span>
<span
class="flex items-center gap-2 px-5 py-3 rounded-[14px] text-[11px] font-extrabold tracking-wider bg-white/10 text-white/70 border border-white/10 uppercase"
class="flex items-center gap-2 px-5 py-3 rounded-[14px] text-[11px] lg:text-[12px] font-extrabold tracking-wider bg-white/10 text-white/80 border border-white/20 uppercase backdrop-blur-sm"
>
<Icon icon="ri:calendar-check-fill" width="18" />
{new Date(incident.created_at).toLocaleDateString()}
</span>
</div>
<!-- Verdict -->
<div class="bg-white/4 border border-white/6 rounded-[20px] p-6">
<div
class="bg-white/5 backdrop-blur-sm border border-white/10 rounded-[20px] p-7"
>
<h3
class="flex items-center gap-[10px] text-white text-base font-bold mb-4"
class="flex items-center gap-2.5 text-white text-base lg:text-lg font-bold mb-4"
>
<Icon icon="ri:scales-3-fill" width="20" />
<Icon icon="ri:scales-3-fill" width="22" />
Verdict
</h3>
<p
class="text-amber-400 text-[18px] font-semibold m-0 leading-normal"
class="text-amber-300 text-[18px] lg:text-[19px] font-semibold m-0 leading-normal"
>
{incident.analysis?.verdict || "Greenwashing detected"}
</p>
</div>
<!-- Detailed Analysis -->
<div class="bg-white/4 border border-white/6 rounded-[20px] p-6">
<div
class="bg-white/5 backdrop-blur-sm border border-white/10 rounded-[20px] p-7"
>
<h3
class="flex items-center gap-[10px] text-white text-base font-bold mb-4"
class="flex items-center gap-2.5 text-white text-base lg:text-lg font-bold mb-4"
>
<Icon icon="ri:file-text-fill" width="20" />
<Icon icon="ri:file-text-fill" width="22" />
Detailed Analysis
</h3>
<p class="text-white/85 text-[15px] leading-[1.7] m-0">
<p
class="text-white/85 text-[15px] lg:text-base leading-[1.7] m-0"
>
{incident.analysis?.reasoning ||
"No detailed analysis available."}
</p>
</div>
<!-- Red Flags -->
{#if incident.analysis?.red_flags && incident.analysis.red_flags.length > 0}
<div
class="bg-white/4 border border-white/[0.06] rounded-[20px] p-6"
class="bg-white/5 backdrop-blur-sm border border-white/10 rounded-[20px] p-7"
>
<h3
class="flex items-center gap-[10px] text-red-400 text-base font-bold mb-4"
class="flex items-center gap-2.5 text-red-300 text-base lg:text-lg font-bold mb-4"
>
<Icon icon="ri:flag-fill" width="20" />
<Icon icon="ri:flag-fill" width="22" />
Red Flags Identified
</h3>
<ul class="list-none p-0 m-0 flex flex-col gap-3">
<ul class="list-none p-0 m-0 flex flex-col gap-3.5">
{#each incident.analysis.red_flags as flag}
<li
class="flex items-start gap-3 text-red-300/80 text-sm leading-[1.6]"
class="flex items-start gap-3 text-red-200/80 text-sm lg:text-base leading-[1.6]"
>
<Icon
icon="ri:error-warning-line"
@@ -154,21 +190,21 @@
</div>
{/if}
<!-- Key Claims -->
{#if incident.analysis?.key_claims && incident.analysis.key_claims.length > 0}
<div
class="bg-white/4 border border-white/[0.06] rounded-[20px] p-6"
class="bg-white/5 backdrop-blur-sm border border-white/10 rounded-[20px] p-7"
>
<h3
class="flex items-center gap-[10px] text-white text-base font-bold mb-4"
class="flex items-center gap-2.5 text-white text-base lg:text-lg font-bold mb-4"
>
<Icon icon="ri:chat-quote-fill" width="20" />
<Icon icon="ri:chat-quote-fill" width="22" />
Environmental Claims Made
</h3>
<ul class="list-none p-0 m-0 flex flex-col gap-3">
<ul class="list-none p-0 m-0 flex flex-col gap-3.5">
{#each incident.analysis.key_claims as claim}
<li
class="flex items-start gap-3 text-white/70 text-sm italic leading-[1.6]"
class="flex items-start gap-3 text-white/75 text-sm lg:text-base italic leading-[1.6]"
>
<Icon
icon="ri:double-quotes-l"
@@ -182,36 +218,38 @@
</div>
{/if}
<!-- Recommendations -->
{#if incident.analysis?.recommendations}
<div
class="bg-emerald-500/8 border border-emerald-500/20 rounded-[20px] p-6"
class="bg-emerald-500/12 backdrop-blur-sm border border-emerald-500/30 rounded-[20px] p-7"
>
<h3
class="flex items-center gap-[10px] text-emerald-400 text-base font-bold mb-4"
class="flex items-center gap-2.5 text-emerald-300 text-base lg:text-lg font-bold mb-4"
>
<Icon icon="ri:lightbulb-fill" width="20" />
<Icon icon="ri:lightbulb-fill" width="22" />
Consumer Recommendations
</h3>
<p class="text-emerald-300 text-[15px] leading-[1.6] m-0">
<p
class="text-emerald-200 text-[15px] lg:text-base leading-[1.6] m-0"
>
{incident.analysis.recommendations}
</p>
</div>
{/if}
<!-- User's Original Report -->
{#if incident.user_description}
<div
class="bg-indigo-500/8 border border-indigo-500/20 rounded-[20px] p-6"
class="bg-indigo-500/12 backdrop-blur-sm border border-indigo-500/30 rounded-[20px] p-7"
>
<h3
class="flex items-center gap-[10px] text-indigo-400 text-base font-bold mb-4"
class="flex items-center gap-2.5 text-indigo-300 text-base lg:text-lg font-bold mb-4"
>
<Icon icon="ri:user-voice-fill" width="20" />
<Icon icon="ri:user-voice-fill" width="22" />
Original User Report
</h3>
<p
class="text-indigo-200 text-[15px] italic leading-[1.6] m-0"
class="text-indigo-200 text-[15px] lg:text-base italic leading-[1.6] m-0"
>
"{incident.user_description}"
</p>
@@ -222,7 +260,7 @@
</div>
<style>
/* Custom utility to hide scrollbar if tailwind plugin not present */
.scrollbar-hide {
scrollbar-width: none;
-ms-overflow-style: none;

View File

@@ -43,52 +43,52 @@
</script>
<button
class="group flex items-center gap-6 bg-black/80 backdrop-blur-[30px] border border-white/20 rounded-3xl p-8 w-full text-left cursor-pointer transition-all duration-300 hover:bg-black/90 hover:border-emerald-500/60 hover:-translate-y-1 hover:scale-[1.01] hover:shadow-[0_24px_48px_rgba(0,0,0,0.5)] relative overflow-hidden outline-none"
class="group flex items-center gap-6 lg:gap-8 bg-black/60 backdrop-blur-2xl border border-white/15 rounded-3xl p-7 lg:p-8 w-full text-left cursor-pointer transition-all duration-300 hover:bg-black/70 hover:border-emerald-500/50 hover:-translate-y-1 hover:scale-[1.01] hover:shadow-2xl hover:shadow-black/50 relative overflow-hidden outline-none"
onclick={() => openReport(report)}
>
<div
class="w-16 h-16 bg-emerald-500/10 rounded-[18px] flex items-center justify-center shrink-0 transition-all duration-300 group-hover:bg-emerald-500 group-hover:-rotate-6"
class="w-16 h-16 lg:w-[4.375rem] lg:h-[4.375rem] bg-emerald-500/15 backdrop-blur-sm rounded-[1.125rem] flex items-center justify-center shrink-0 transition-all duration-300 group-hover:bg-emerald-500 group-hover:-rotate-6 group-hover:scale-110"
>
<Icon icon={fileDetails.icon} width="32" class="text-white" />
</div>
<div class="flex-1 min-w-0">
<div class="flex items-center gap-3 mb-2">
<h3 class="text-white text-xl font-extrabold m-0">
<div class="flex items-center gap-3 mb-2.5">
<h3 class="text-white text-xl lg:text-[22px] font-extrabold m-0">
{report.company_name}
</h3>
<span
class="text-emerald-400 bg-emerald-500/10 px-2 py-0.5 rounded-md text-[12px] font-bold"
class="text-emerald-300 bg-emerald-500/15 backdrop-blur-sm px-3 py-1 rounded-lg text-[12px] lg:text-[13px] font-bold"
>{report.year}</span
>
</div>
{#if report.snippet}
<p
class="text-white/60 text-sm leading-relaxed mb-4 line-clamp-2 m-0"
class="text-white/70 text-sm lg:text-base leading-relaxed mb-4 line-clamp-2 m-0"
>
{@html report.snippet.replace(
new RegExp(searchQuery || "", "gi"),
(match) =>
`<span class="text-emerald-400 bg-emerald-500/20 px-0.5 rounded font-semibold">${match}</span>`,
`<span class="text-emerald-300 bg-emerald-500/25 px-1 rounded font-semibold">${match}</span>`,
)}
</p>
{:else}
<p class="text-white/40 text-sm mb-4 m-0">
<p class="text-white/50 text-sm lg:text-base mb-4 m-0">
{report.sector} Sector • Impact Report
</p>
{/if}
<div class="flex flex-wrap gap-2">
<div class="flex flex-wrap gap-2.5">
<span
class="flex items-center gap-1.5 px-3 py-1.5 rounded-xl text-[11px] font-bold tracking-tight bg-white/5 text-white/60 max-w-[200px] truncate"
class="flex items-center gap-1.5 px-3 py-1.5 rounded-xl text-[11px] lg:text-[12px] font-bold tracking-tight bg-white/10 backdrop-blur-sm text-white/70 max-w-xs truncate"
title={report.filename}
>
<Icon icon={fileDetails.icon} width="14" />
<Icon icon={fileDetails.icon} width="15" />
{fileDetails.type}
</span>
<span
class="flex items-center gap-1.5 px-3 py-1.5 rounded-xl text-[11px] font-bold tracking-tight bg-emerald-500/10 text-emerald-400"
class="flex items-center gap-1.5 px-3 py-1.5 rounded-xl text-[11px] lg:text-[12px] font-bold tracking-tight bg-emerald-500/15 backdrop-blur-sm text-emerald-300"
>
<Icon
icon="ri:checkbox-circle-fill"
@@ -101,18 +101,18 @@
</div>
{#if report.greenwashing_score}
<div class="text-center ml-5">
<div class="text-center ml-5 lg:ml-6">
<div
class="w-[52px] h-[52px] rounded-xe flex items-center justify-center mb-1 rounded-[14px] {getScoreColor(
class="w-14 h-14 lg:w-[3.875rem] lg:h-[3.875rem] rounded-xe flex items-center justify-center mb-2 rounded-2xl shadow-lg transition-transform group-hover:scale-110 {getScoreColor(
report.greenwashing_score,
)}"
>
<span class="text-emerald-950 text-[18px] font-black"
<span class="text-emerald-950 text-[19px] lg:text-[21px] font-black"
>{Math.round(Number(report.greenwashing_score))}</span
>
</div>
<span
class="text-white/40 text-[10px] font-extrabold uppercase tracking-widest"
class="text-white/50 text-[10px] lg:text-[11px] font-extrabold uppercase tracking-widest"
>Trust Score</span
>
</div>

View File

@@ -0,0 +1,45 @@
<script lang="ts">
import Icon from "@iconify/svelte";
let {
inputText = $bindable(),
isLoading,
onSend,
} = $props<{
inputText: string;
isLoading: boolean;
onSend: () => void;
}>();
function handleKeyDown(event: KeyboardEvent) {
if (event.key === "Enter" && !event.shiftKey) {
event.preventDefault();
onSend();
}
}
</script>
<div
class="p-4 pb-8 md:pb-4 bg-[#051f18] md:bg-white/5 border-t border-[#1f473b] md:border-white/10 flex items-center gap-3 w-full"
>
<input
type="text"
class="flex-1 bg-[#0d2e25] md:bg-white/10 text-white p-3.5 px-5 border border-[#1f473b] md:border-white/10 rounded-full text-[15px] outline-none transition-all duration-200 placeholder-gray-500 focus:border-emerald-400 focus:bg-[#11382e] md:focus:bg-white/15"
placeholder="Ask about sustainability..."
bind:value={inputText}
onkeydown={handleKeyDown}
disabled={isLoading}
/>
<button
class="bg-emerald-500 w-12 h-12 rounded-full flex items-center justify-center text-white shadow-lg transition-all duration-200 hover:scale-105 hover:shadow-emerald-500/50 disabled:opacity-50 disabled:hover:scale-100 disabled:cursor-not-allowed"
onclick={onSend}
aria-label="Send message"
disabled={isLoading}
>
{#if isLoading}
<Icon icon="ri:loader-4-line" width="24" class="animate-spin" />
{:else}
<Icon icon="ri:send-plane-fill" width="24" />
{/if}
</button>
</div>

View File

@@ -0,0 +1,43 @@
<script lang="ts">
import { marked } from "marked";
let { text, sender } = $props<{
text: string;
sender: "user" | "ai";
}>();
</script>
<div
class="p-3 px-5 rounded-[20px] max-w-[85%] text-[15px] leading-relaxed transition-all duration-200
{sender === 'user'
? 'self-end bg-gradient-to-br from-emerald-500 to-emerald-600 text-white rounded-br-md shadow-lg shadow-emerald-500/20'
: 'self-start bg-teal-900/40 border border-white/10 text-white rounded-bl-md shadow-sm backdrop-blur-sm'}"
>
<div
class="message-content prose prose-invert prose-p:my-1 prose-headings:my-2 prose-strong:text-white prose-ul:my-1 max-w-none"
>
{@html marked.parse(text)}
</div>
</div>
<style>
:global(.message-content p) {
margin: 0 0 0.5rem 0;
}
:global(.message-content p:last-child) {
margin: 0;
}
:global(.message-content strong) {
font-weight: 700;
color: inherit;
}
:global(.message-content ul),
:global(.message-content ol) {
margin: 0.25rem 0 0.5rem 1.25rem;
padding: 0;
}
:global(.message-content li) {
margin-bottom: 0.25rem;
}
</style>

View File

@@ -0,0 +1,14 @@
<div
class="self-start bg-teal-900/40 border border-white/10 rounded-bl-md rounded-[20px] p-3 px-4 w-fit shadow-sm backdrop-blur-sm"
>
<div class="flex items-center gap-1">
<span
class="w-1.5 h-1.5 bg-emerald-400 rounded-full animate-bounce [animation-delay:-0.32s]"
></span>
<span
class="w-1.5 h-1.5 bg-emerald-400 rounded-full animate-bounce [animation-delay:-0.16s]"
></span>
<span class="w-1.5 h-1.5 bg-emerald-400 rounded-full animate-bounce"
></span>
</div>
</div>

View File

@@ -0,0 +1,73 @@
<script lang="ts">
import { onMount } from "svelte";
let canvasElement = $state<HTMLCanvasElement>();
onMount(() => {
if (!canvasElement) return;
const ctx = canvasElement.getContext("2d");
if (!ctx) return;
let frame = 0;
let animationId: number;
function animate() {
frame++;
if (!ctx || !canvasElement) return;
ctx.clearRect(0, 0, 40, 40);
const yOffset = Math.sin(frame * 0.05) * 3;
ctx.fillStyle = "#e0e0e0";
ctx.beginPath();
ctx.arc(20, 20, 18, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = "#22c55e";
ctx.beginPath();
ctx.arc(20, 20 + yOffset, 14, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = "#16a34a";
ctx.beginPath();
ctx.moveTo(20, 8 + yOffset);
ctx.quadraticCurveTo(28, 4 + yOffset, 24, 16 + yOffset);
ctx.closePath();
ctx.fill();
ctx.fillStyle = "white";
ctx.fillRect(14, 16 + yOffset, 3, 5);
ctx.fillRect(23, 16 + yOffset, 3, 5);
ctx.strokeStyle = "white";
ctx.lineWidth = 2;
ctx.beginPath();
ctx.arc(20, 26 + yOffset, 4, 0.2, Math.PI - 0.2);
ctx.stroke();
animationId = requestAnimationFrame(animate);
}
animate();
return () => {
if (animationId) cancelAnimationFrame(animationId);
};
});
</script>
<div class="relative w-10 h-10">
<canvas
bind:this={canvasElement}
width="40"
height="40"
class="w-10 h-10 drop-shadow-[0_4px_12px_rgba(16,185,129,0.4)]"
></canvas>
<div
class="absolute bottom-0.5 right-0.5 w-2.5 h-2.5 bg-emerald-400 border-2 border-[#051f18] rounded-full shadow-[0_0_8px_rgba(52,211,153,0.6)]"
></div>
</div>

View File

@@ -8,13 +8,12 @@
import IncidentCard from "$lib/components/catalogue/IncidentCard.svelte";
import CompanyModal from "$lib/components/catalogue/CompanyModal.svelte";
import IncidentModal from "$lib/components/catalogue/IncidentModal.svelte";
import Pagination from "$lib/components/catalogue/Pagination.svelte";
// View mode toggle
type ViewMode = "company" | "user";
let viewMode = $state<ViewMode>("company");
// Data Types
interface Report {
company_name: string;
year: string | number;
@@ -31,6 +30,8 @@
detected_brand: string;
user_description?: string;
created_at: string;
image_base64?: string;
report_type?: "product" | "company";
analysis: {
verdict: string;
confidence: string;
@@ -48,7 +49,7 @@
let searchQuery = $state("");
let isLoading = $state(false);
// Predefined categories
const categories = [
"All",
"Tech",
@@ -61,7 +62,7 @@
];
let selectedCategory = $state("All");
// Fetching logic
async function fetchReports() {
isLoading = true;
try {
@@ -93,7 +94,7 @@
fetchIncidents();
});
// Search
async function handleSearch() {
if (!searchQuery.trim()) {
fetchReports();
@@ -124,7 +125,7 @@
debounceTimer = setTimeout(handleSearch, 600);
}
// Pagination & Filtering
let currentPage = $state(1);
const itemsPerPage = 10;
@@ -154,11 +155,10 @@
function goToPage(page: number) {
if (page >= 1 && page <= totalPages) {
currentPage = page;
window.scrollTo({ top: 0, behavior: "smooth" });
}
}
// Modals
let selectedReport = $state<Report | null>(null);
let selectedIncident = $state<Incident | null>(null);
</script>
@@ -172,7 +172,7 @@
</svelte:head>
<div class="relative w-full min-h-screen overflow-x-hidden">
<!-- Modals -->
{#if selectedReport}
<CompanyModal
report={selectedReport}
@@ -193,7 +193,7 @@
</div>
<div
class="relative z-10 px-6 pt-[100px] pb-[120px] max-w-[1000px] mx-auto"
class="relative z-10 px-6 sm:px-8 lg:px-12 pt-16 pb-35 max-w-275 mx-auto"
>
<CatalogueHeader
bind:viewMode
@@ -201,30 +201,35 @@
bind:selectedCategory
{categories}
{onSearchInput}
{currentPage}
{totalPages}
{goToPage}
/>
{#if isLoading}
<div class="flex flex-col items-center justify-center py-20 gap-4">
<div
class="flex flex-col items-center justify-center py-24 gap-5 bg-black/60 backdrop-blur-2xl rounded-3xl border border-white/15 shadow-2xl shadow-black/50 mt-8"
>
<Icon
icon="eos-icons:loading"
width="40"
width="48"
class="text-emerald-400"
/>
<p class="text-white/60 font-medium">
<p class="text-white/70 font-medium text-lg">
Syncing with database...
</p>
</div>
{:else if viewMode === "company"}
{#if filteredReports.length === 0}
<div
class="bg-black/40 backdrop-blur-md rounded-3xl p-12 text-center border border-white/10"
class="bg-black/60 backdrop-blur-2xl rounded-3xl p-16 text-center border border-white/15 shadow-2xl shadow-black/50 mt-8"
>
<p class="text-white/60 text-lg">
<p class="text-white/70 text-lg font-medium">
No reports found matching your criteria.
</p>
</div>
{:else}
<div class="flex flex-col gap-5">
<div class="flex flex-col gap-6 mt-8">
{#each paginatedReports as report}
<ReportCard
{report}
@@ -233,24 +238,25 @@
/>
{/each}
</div>
<Pagination bind:currentPage {totalPages} {goToPage} />
{/if}
{:else if incidents.length === 0}
<div
class="bg-black/40 backdrop-blur-md rounded-3xl p-20 text-center border border-white/10 flex flex-col items-center gap-4"
class="bg-black/60 backdrop-blur-2xl rounded-3xl p-20 text-center border border-white/15 shadow-2xl shadow-black/50 flex flex-col items-center gap-5 mt-8"
>
<Icon
icon="ri:file-warning-line"
width="48"
width="56"
class="text-white/30"
/>
<p class="text-white/60 text-lg">No user reports yet.</p>
<p class="text-white/40 text-sm">
<p class="text-white/70 text-lg font-medium">
No user reports yet.
</p>
<p class="text-white/50 text-sm">
Be the first to report greenwashing!
</p>
</div>
{:else}
<div class="flex flex-col gap-5">
<div class="flex flex-col gap-6 mt-8">
{#each incidents as incident}
<IncidentCard
{incident}
@@ -264,7 +270,7 @@
<style>
:global(body) {
background-color: #051010;
background-color: #0c0c0c;
color: white;
}
</style>

View File

@@ -1,10 +1,18 @@
<script lang="ts">
import { onMount } from "svelte";
import ParallaxLandscape from "$lib/components/ParallaxLandscape.svelte";
import Icon from "@iconify/svelte";
import { marked } from "marked";
import ChatMessage from "$lib/components/chat/ChatMessage.svelte";
import ChatInput from "$lib/components/chat/ChatInput.svelte";
import Mascot from "$lib/components/chat/Mascot.svelte";
import LoadingBubble from "$lib/components/chat/LoadingBubble.svelte";
let messages = $state([
type Message = {
id: number;
text: string;
sender: "user" | "ai";
};
let messages = $state<Message[]>([
{
id: 1,
text: "Hello! I'm Ethix AI. Ask me anything about recycling, sustainability, or green products.",
@@ -12,9 +20,8 @@
},
]);
let inputText = $state("");
let canvasElement = $state<HTMLCanvasElement>();
let isLoading = $state(false);
let chatWindowFn: HTMLDivElement | undefined = $state();
let chatWindowFn = $state<HTMLDivElement>();
function scrollToBottom() {
if (chatWindowFn) {
@@ -25,7 +32,7 @@
}
$effect(() => {
// Dependencies to trigger scroll
messages;
isLoading;
scrollToBottom();
@@ -38,7 +45,7 @@
const userMsg = {
id: Date.now(),
text: userText,
sender: "user",
sender: "user" as const,
};
messages = [...messages, userMsg];
inputText = "";
@@ -49,12 +56,8 @@
"http://localhost:5000/api/gemini/ask",
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
prompt: userText,
}),
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ prompt: userText }),
},
);
@@ -64,7 +67,7 @@
const aiMsg = {
id: Date.now() + 1,
text: data.reply,
sender: "ai",
sender: "ai" as const,
};
messages = [...messages, aiMsg];
} else {
@@ -74,7 +77,7 @@
const errorMsg = {
id: Date.now() + 1,
text: "Sorry, I'm having trouble connecting to my brain right now. Please try again later.",
sender: "ai",
sender: "ai" as const,
};
messages = [...messages, errorMsg];
console.error("Chat Error:", error);
@@ -82,63 +85,6 @@
isLoading = false;
}
}
function handleKeyDown(event: KeyboardEvent) {
if (event.key === "Enter" && !event.shiftKey) {
event.preventDefault();
sendMessage();
}
}
onMount(() => {
if (!canvasElement) return;
const ctx = canvasElement.getContext("2d");
if (!ctx) return;
let frame = 0;
function animate() {
frame++;
if (!ctx || !canvasElement) return;
ctx.clearRect(0, 0, 40, 40);
const yOffset = Math.sin(frame * 0.05) * 3; // Reduced amplitude
// Head
ctx.fillStyle = "#e0e0e0";
ctx.beginPath();
ctx.arc(20, 20, 18, 0, Math.PI * 2); // Center at 20,20, Radius 18
ctx.fill();
// Face/Visor
ctx.fillStyle = "#22c55e";
ctx.beginPath();
ctx.arc(20, 20 + yOffset, 14, 0, Math.PI * 2); // Center 20,20
ctx.fill();
// Reflection/Detail
ctx.fillStyle = "#16a34a";
ctx.beginPath();
ctx.moveTo(20, 8 + yOffset);
ctx.quadraticCurveTo(28, 4 + yOffset, 24, 16 + yOffset);
ctx.closePath();
ctx.fill();
// Eyes
ctx.fillStyle = "white";
ctx.fillRect(14, 16 + yOffset, 3, 5);
ctx.fillRect(23, 16 + yOffset, 3, 5);
// Smile
ctx.strokeStyle = "white";
ctx.lineWidth = 2;
ctx.beginPath();
ctx.arc(20, 26 + yOffset, 4, 0.2, Math.PI - 0.2);
ctx.stroke();
requestAnimationFrame(animate);
}
animate();
});
</script>
<svelte:head>
@@ -149,426 +95,68 @@
/>
</svelte:head>
<div class="page-wrapper">
<div class="desktop-bg">
<div class="fixed inset-0 w-full h-full overflow-hidden bg-[#0c0c0c]">
<div class="hidden md:block absolute inset-0 pointer-events-none">
<ParallaxLandscape />
</div>
<div class="chat-container">
<div class="chat-card">
<div class="header">
<div class="mascot-container">
<canvas
bind:this={canvasElement}
width="40"
height="40"
class="mascot-canvas"
></canvas>
<div class="mascot-status-dot"></div>
</div>
<div class="header-text-center">
<h1 class="page-title">Ethix Assistant</h1>
<div class="powered-by">
<div
class="relative z-10 max-w-6xl mx-auto h-full flex flex-col pt-24 pb-6 px-0 md:px-6 md:pt-20 md:pb-10"
>
<div
class="flex flex-col h-full bg-[#0d2e25] md:bg-black/40 md:backdrop-blur-xl border-x md:border border-[#1f473b] md:border-white/10 md:rounded-[32px] overflow-hidden shadow-2xl"
>
<div
class="p-3 px-5 border-b border-[#1f473b] md:border-white/10 bg-[#051f18] md:bg-transparent flex items-center gap-3 shrink-0"
>
<Mascot />
<div class="flex flex-col items-start gap-0.5">
<h1
class="text-white text-base font-bold leading-tight m-0"
>
Ethix Assistant
</h1>
<div
class="flex items-center gap-1 text-[10px] font-semibold text-emerald-400 tracking-wide bg-emerald-500/10 px-2 py-0.5 rounded-xl border border-emerald-500/20"
>
<Icon icon="ri:shining-fill" width="10" />
<span>Powered by Gemini</span>
</div>
</div>
</div>
<div class="chat-window">
<div class="messages-container" bind:this={chatWindowFn}>
<div
class="flex-1 flex flex-col overflow-hidden bg-[#0d2e25] md:bg-transparent"
>
<div
class="flex-1 overflow-y-auto p-6 pb-5 flex flex-col gap-4 scroll-smooth scrollbar-none"
bind:this={chatWindowFn}
>
{#each messages as msg (msg.id)}
<div
class="message"
class:user-message={msg.sender === "user"}
class:ai-message={msg.sender === "ai"}
>
<div class="message-content">
{@html marked.parse(msg.text)}
</div>
</div>
<ChatMessage text={msg.text} sender={msg.sender} />
{/each}
{#if isLoading}
<div class="message ai-message loading-bubble">
<div class="typing-indicator">
<span></span>
<span></span>
<span></span>
</div>
</div>
<LoadingBubble />
{/if}
</div>
<div class="input-container">
<input
type="text"
class="message-input"
placeholder="Ask about sustainability..."
bind:value={inputText}
onkeydown={handleKeyDown}
/>
<button
class="send-button"
onclick={sendMessage}
aria-label="Send message"
>
<Icon icon="ri:send-plane-fill" width="24" />
</button>
</div>
<ChatInput bind:inputText {isLoading} onSend={sendMessage} />
</div>
</div>
</div>
</div>
<style>
.page-wrapper {
width: 100%;
height: 100vh;
position: relative;
overflow: hidden;
}
.desktop-bg {
.scrollbar-none::-webkit-scrollbar {
display: none;
}
.chat-container {
position: relative;
z-index: 10;
padding: 100px 24px 40px;
max-width: 800px;
margin: 0 auto;
height: 100vh;
box-sizing: border-box;
display: flex;
flex-direction: column;
}
.chat-card {
background: #0d2e25;
border: 1px solid #1f473b;
border-radius: 32px;
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.3);
overflow: hidden;
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
}
.header {
padding: 12px 20px;
border-bottom: 1px solid #1f473b;
background: #051f18;
display: flex;
flex-direction: row; /* Horizontal Layout for Compactness */
align-items: center;
justify-content: flex-start;
gap: 12px;
}
.header-text-center {
display: flex;
flex-direction: column;
align-items: flex-start; /* Left Align Text */
gap: 2px;
}
.mascot-container {
position: relative;
width: 40px;
height: 40px;
}
.mascot-canvas {
width: 40px;
height: 40px;
filter: drop-shadow(0 4px 12px rgba(16, 185, 129, 0.4));
}
.mascot-status-dot {
position: absolute;
bottom: 2px;
right: 2px;
width: 10px;
height: 10px;
background: #34d399;
border: 2px solid #051f18;
border-radius: 50%;
box-shadow: 0 0 8px rgba(52, 211, 153, 0.6);
}
.page-title {
color: white;
font-size: 16px;
font-weight: 700;
margin: 0;
line-height: 1.2;
}
.powered-by {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 10px;
font-weight: 600;
color: #34d399;
letter-spacing: 0.5px;
background: rgba(34, 197, 94, 0.1);
padding: 2px 8px;
border-radius: 12px;
border: 1px solid rgba(34, 197, 94, 0.2);
}
.chat-window {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
background: #0d2e25;
}
.messages-container {
flex: 1;
overflow-y: auto;
padding: 24px;
padding-bottom: 20px;
display: flex;
flex-direction: column;
gap: 16px;
scroll-behavior: smooth;
}
.message {
padding: 12px 18px;
border-radius: 20px;
max-width: 85%;
font-size: 15px;
line-height: 1.5;
}
/* Message Content Markdown Styles */
.message-content :global(p) {
margin: 0 0 8px 0;
}
.message-content :global(p:last-child) {
margin: 0;
}
.message-content :global(strong) {
font-weight: 700;
color: inherit;
}
.message-content :global(ul),
.message-content :global(ol) {
margin: 4px 0 8px 20px;
padding: 0;
}
.message-content :global(li) {
margin-bottom: 4px;
}
.message-content :global(h1),
.message-content :global(h2),
.message-content :global(h3) {
font-weight: 700;
font-size: 1.1em;
margin: 8px 0 4px 0;
}
.user-message {
align-self: flex-end;
background: linear-gradient(135deg, #10b981, #059669);
border-bottom-right-radius: 6px;
color: white;
font-weight: 500;
box-shadow: 0 4px 12px rgba(16, 185, 129, 0.2);
}
.ai-message {
align-self: flex-start;
background: #134e4a; /* Teal-900 for better visibility */
border-bottom-left-radius: 6px;
color: white; /* White text for contrast */
border: 1px solid rgba(255, 255, 255, 0.1);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
}
/* Loading Bubble */
.loading-bubble {
padding: 12px 16px;
width: fit-content;
}
.typing-indicator {
display: flex;
align-items: center;
gap: 4px;
}
.typing-indicator span {
width: 6px;
height: 6px;
background-color: #34d399;
border-radius: 50%;
display: inline-block;
animation: bounce 1.4s infinite ease-in-out both;
}
.typing-indicator span:nth-child(1) {
animation-delay: -0.32s;
}
.typing-indicator span:nth-child(2) {
animation-delay: -0.16s;
}
@keyframes bounce {
0%,
80%,
100% {
transform: scale(0);
}
40% {
transform: scale(1);
}
}
.input-container {
padding: 16px 20px;
padding-bottom: 30px;
background: #051f18;
border-top: 1px solid #1f473b;
display: flex;
align-items: center;
gap: 12px;
}
.message-input {
flex: 1;
background: #0d2e25;
color: white;
padding: 14px 20px;
border: 1px solid #1f473b;
border-radius: 50px;
font-size: 15px;
outline: none;
transition: all 0.2s;
}
.message-input:focus {
border-color: #34d399;
background: #11382e;
}
.message-input::placeholder {
color: #6b7280;
}
.send-button {
background: #10b981;
width: 48px;
height: 48px;
border-radius: 50%;
border: none;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
transition: all 0.2s ease;
flex-shrink: 0;
color: white;
}
.send-button:hover {
transform: scale(1.08);
box-shadow: 0 0 20px rgba(16, 185, 129, 0.5);
}
@media (min-width: 768px) {
.desktop-bg {
display: block;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
}
.chat-container {
padding-top: 100px;
height: auto;
max-width: 1200px;
}
.chat-card {
background: rgba(0, 0, 0, 0.4);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 32px;
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.3);
height: 85vh;
max-height: 900px;
}
.header {
background: transparent;
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
}
.chat-window {
background: transparent;
}
.ai-message {
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.1);
}
.input-container {
background: rgba(255, 255, 255, 0.05);
border-top: 1px solid rgba(255, 255, 255, 0.08);
padding-bottom: 16px;
}
.message-input {
background: rgba(255, 255, 255, 0.08);
border: 1px solid rgba(255, 255, 255, 0.1);
}
.message-input:focus {
background: rgba(255, 255, 255, 0.12);
border-color: rgba(34, 197, 94, 0.5);
}
}
@media (max-width: 767px) {
.chat-container {
padding: 0;
max-width: 100%;
height: 100vh;
display: flex;
flex-direction: column;
}
.chat-card {
height: 100%;
max-height: none;
border-radius: 0;
border: none;
}
.header {
padding: 12px 16px;
padding-top: 50px; /* Safe area for some mobile devices */
}
.messages-container {
padding-bottom: 20px;
}
.input-container {
padding-bottom: 120px;
}
.scrollbar-none {
-ms-overflow-style: none;
scrollbar-width: none;
}
</style>

View File

@@ -30,7 +30,11 @@
{#if item}
<div class="safe-area">
<div class="header">
<button class="back-button" on:click={() => window.history.back()}>
<button
class="back-button"
onclick={() => window.history.back()}
aria-label="Go back"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 320 512"
@@ -65,7 +69,9 @@
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 512 512"
fill={item.impact === "High" ? "#ef4444" : "#22c55e"}
fill={item.impact === "High"
? "#ef4444"
: "#22c55e"}
class="alert-icon"
>
<path
@@ -76,17 +82,21 @@
<h3>Analysis Result</h3>
<p>
{#if item.impact === "High"}
This item takes 450+ years to decompose. Consider switching
to sustainable alternatives immediately.
This item takes 450+ years to decompose.
Consider switching to sustainable
alternatives immediately.
{:else}
This item is eco-friendly or easily recyclable. Good choice!
This item is eco-friendly or easily
recyclable. Good choice!
{/if}
</p>
</div>
</div>
<div class="alternatives-section">
<h3 class="alternatives-title">Recommended Alternatives</h3>
<h3 class="alternatives-title">
Recommended Alternatives
</h3>
<div class="alternatives-scroll">
<div class="alternative-card glass">
<div class="alt-header">
@@ -142,7 +152,10 @@
</div>
{#if item.impact !== "Low"}
<button class="report-button" on:click={navigateToReport}>
<button
class="report-button"
onclick={navigateToReport}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 448 512"

View File

@@ -4,6 +4,9 @@
let productName = $state("");
let description = $state("");
let image = $state<string | null>(null);
let reportType = $state<"product" | "company">("product");
let pdfData = $state<string | null>(null);
let pdfName = $state<string | null>(null);
let submitted = $state(false);
let isLoading = $state(false);
let analysisResult = $state<any>(null);
@@ -40,6 +43,25 @@
input.click();
}
async function pickPdf() {
const input = document.createElement("input");
input.type = "file";
input.accept = "application/pdf";
input.onchange = (e: Event) => {
const target = e.target as HTMLInputElement;
const file = target.files?.[0];
if (file) {
pdfName = file.name;
const reader = new FileReader();
reader.onload = (event) => {
pdfData = event.target?.result as string;
};
reader.readAsDataURL(file);
}
};
input.click();
}
const progressSteps = [
{ id: 1, label: "Scanning image...", icon: "ri:camera-lens-line" },
{
@@ -65,7 +87,7 @@
error = null;
currentStep = 1;
// Simulate progress steps
const stepInterval = setInterval(() => {
if (currentStep < progressSteps.length) {
currentStep++;
@@ -83,18 +105,20 @@
body: JSON.stringify({
product_name: productName,
description: description,
image: image, // Base64 encoded
report_type: reportType,
image: reportType === "product" ? image : null,
pdf_data: reportType === "company" ? pdfData : null,
}),
},
);
clearInterval(stepInterval);
currentStep = progressSteps.length; // Complete all steps
currentStep = progressSteps.length;
const data = await response.json();
if (data.status === "success") {
// Brief pause to show completion
await new Promise((r) => setTimeout(r, 500));
analysisResult = data;
submitted = true;
@@ -200,10 +224,28 @@
<div class="glass-card form-card">
<div class="form-content">
<div class="form-group">
<label class="label" for="productName"
>Product Name</label
<div class="report-type-toggle">
<button
class="toggle-option"
class:active={reportType === "product"}
onclick={() => (reportType = "product")}
>
Product Incident
</button>
<button
class="toggle-option"
class:active={reportType === "company"}
onclick={() => (reportType = "company")}
>
Company Report
</button>
</div>
<div class="form-group">
<label class="label" for="productName">
{reportType === "product"
? "Product Name"
: "Company Name"}
</label>
<div class="input-wrapper">
<iconify-icon
icon="ri:price-tag-3-line"
@@ -213,7 +255,9 @@
type="text"
id="productName"
class="input"
placeholder="e.g. 'Eco-Friendly' Water Bottle"
placeholder={reportType === "product"
? "e.g. 'Eco-Friendly' Water Bottle"
: "e.g. Acme Corp"}
bind:value={productName}
/>
</div>
@@ -238,35 +282,70 @@
</div>
<div class="form-group">
<span class="label">Evidence (Photo)</span>
<button
class="image-picker"
onclick={pickImage}
type="button"
>
{#if image}
<img
src={image}
alt="Evidence"
class="picked-image"
/>
<div class="change-overlay">
<iconify-icon
icon="ri:camera-switch-line"
width="24"
></iconify-icon>
</div>
{:else}
<div class="picker-placeholder">
<iconify-icon
icon="ri:camera-add-line"
width="32"
style="color: rgba(255,255,255,0.4);"
></iconify-icon>
<p>Upload Photo</p>
</div>
{/if}
</button>
<span class="label">
{reportType === "product"
? "Evidence (Photo)"
: "Company Report (PDF)"}
</span>
{#if reportType === "product"}
<button
class="image-picker"
onclick={pickImage}
type="button"
>
{#if image}
<img
src={image}
alt="Evidence"
class="picked-image"
/>
<div class="change-overlay">
<iconify-icon
icon="ri:camera-switch-line"
width="24"
></iconify-icon>
</div>
{:else}
<div class="picker-placeholder">
<iconify-icon
icon="ri:camera-add-line"
width="32"
style="color: rgba(255,255,255,0.4);"
></iconify-icon>
<p>Upload Photo</p>
</div>
{/if}
</button>
{:else}
<button
class="image-picker pdf-picker"
onclick={pickPdf}
type="button"
>
{#if pdfData}
<div class="picker-placeholder active-pdf">
<iconify-icon
icon="ri:file-pdf-2-fill"
width="48"
style="color: #ef4444;"
></iconify-icon>
<p class="pdf-name">{pdfName}</p>
<span class="change-text"
>Click to change</span
>
</div>
{:else}
<div class="picker-placeholder">
<iconify-icon
icon="ri:file-upload-line"
width="32"
style="color: rgba(255,255,255,0.4);"
></iconify-icon>
<p>Upload PDF Report</p>
</div>
{/if}
</button>
{/if}
</div>
{#if error}
@@ -410,6 +489,51 @@
gap: 16px;
}
.report-type-toggle {
display: flex;
background: rgba(255, 255, 255, 0.05);
border-radius: 12px;
padding: 4px;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.toggle-option {
flex: 1;
background: transparent;
border: none;
color: rgba(255, 255, 255, 0.6);
padding: 10px;
border-radius: 8px;
font-weight: 600;
font-size: 13px;
cursor: pointer;
transition: all 0.2s;
}
.toggle-option:hover {
color: white;
background: rgba(255, 255, 255, 0.05);
}
.toggle-option.active {
background: rgba(34, 197, 94, 0.15);
color: #4ade80;
}
.pdf-name {
color: white !important;
font-size: 14px;
text-align: center;
padding: 0 20px;
word-break: break-all;
}
.change-text {
font-size: 12px;
color: rgba(255, 255, 255, 0.4);
margin-top: 4px;
}
.form-group {
display: flex;
flex-direction: column;