mirror of
https://github.com/SirBlobby/Hoya26.git
synced 2026-02-04 11:44:34 -05:00
Restore code and save recent updates
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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'));
|
||||
});
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -201,7 +201,7 @@
|
||||
const config = activeConfig();
|
||||
|
||||
if (!config.staticScene || config.scenes) {
|
||||
// Always use window scroll now
|
||||
|
||||
scrollContainer = null;
|
||||
updateMeasurements();
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
45
frontend/src/lib/components/chat/ChatInput.svelte
Normal file
45
frontend/src/lib/components/chat/ChatInput.svelte
Normal 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>
|
||||
43
frontend/src/lib/components/chat/ChatMessage.svelte
Normal file
43
frontend/src/lib/components/chat/ChatMessage.svelte
Normal 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>
|
||||
14
frontend/src/lib/components/chat/LoadingBubble.svelte
Normal file
14
frontend/src/lib/components/chat/LoadingBubble.svelte
Normal 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>
|
||||
73
frontend/src/lib/components/chat/Mascot.svelte
Normal file
73
frontend/src/lib/components/chat/Mascot.svelte
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user