mirror of
https://github.com/SirBlobby/Hoya26.git
synced 2026-02-04 03:34:34 -05:00
Database Viewer Update
This commit is contained in:
101
frontend/src/lib/components/catalogue/CatalogueHeader.svelte
Normal file
101
frontend/src/lib/components/catalogue/CatalogueHeader.svelte
Normal file
@@ -0,0 +1,101 @@
|
||||
<script lang="ts">
|
||||
import Icon from "@iconify/svelte";
|
||||
|
||||
let {
|
||||
viewMode = $bindable(),
|
||||
searchQuery = $bindable(),
|
||||
selectedCategory = $bindable(),
|
||||
categories,
|
||||
onSearchInput,
|
||||
}: {
|
||||
viewMode: "company" | "user";
|
||||
searchQuery: string;
|
||||
selectedCategory: string;
|
||||
categories: string[];
|
||||
onSearchInput: () => 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)]"
|
||||
>
|
||||
<!-- Header content -->
|
||||
<div class="mb-8 px-3 text-center">
|
||||
<h1 class="text-white text-[42px] font-black m-0 tracking-[-2px]">
|
||||
Sustainability Database
|
||||
</h1>
|
||||
<p class="text-white/70 text-base mt-2 font-medium">
|
||||
{#if viewMode === "company"}
|
||||
Search within verified company reports and impact assessments
|
||||
{:else}
|
||||
Browse user-submitted greenwashing reports
|
||||
{/if}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- View Mode Toggle Switch -->
|
||||
<div class="flex items-center justify-center gap-4 my-6">
|
||||
<span
|
||||
class="flex items-center gap-1.5 text-sm font-semibold transition-all duration-300 {viewMode ===
|
||||
'company'
|
||||
? 'text-emerald-400'
|
||||
: 'text-white/40'}"
|
||||
>
|
||||
<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"
|
||||
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 ===
|
||||
'user'
|
||||
? 'left-[calc(100%-25px)]'
|
||||
: 'left-[3px]'}"
|
||||
></span>
|
||||
</button>
|
||||
<span
|
||||
class="flex items-center gap-1.5 text-sm font-semibold transition-all duration-300 {viewMode ===
|
||||
'user'
|
||||
? 'text-emerald-400'
|
||||
: 'text-white/40'}"
|
||||
>
|
||||
<Icon icon="ri:user-voice-line" width="16" />
|
||||
User Reports
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{#if viewMode === "company"}
|
||||
<div class="relative max-w-[600px] mx-auto mb-8">
|
||||
<div
|
||||
class="absolute left-5 top-1/2 -translate-y-1/2 text-white/60 flex items-center pointer-events-none"
|
||||
>
|
||||
<Icon icon="ri:search-line" width="20" />
|
||||
</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"
|
||||
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">
|
||||
{#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 ===
|
||||
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'}"
|
||||
onclick={() => (selectedCategory = category)}
|
||||
>
|
||||
{category}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
86
frontend/src/lib/components/catalogue/CompanyModal.svelte
Normal file
86
frontend/src/lib/components/catalogue/CompanyModal.svelte
Normal file
@@ -0,0 +1,86 @@
|
||||
<script lang="ts">
|
||||
import Icon from "@iconify/svelte";
|
||||
import { fade, scale } from "svelte/transition";
|
||||
|
||||
interface Report {
|
||||
company_name: string;
|
||||
year: string | number;
|
||||
sector: string;
|
||||
greenwashing_score: number | string;
|
||||
filename: string;
|
||||
title?: string;
|
||||
snippet?: string;
|
||||
}
|
||||
|
||||
let { report, onclose }: { report: Report; onclose: () => void } = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="fixed inset-0 bg-black/80 backdrop-blur-md z-1000 flex justify-center items-center p-5 outline-none"
|
||||
onclick={onclose}
|
||||
onkeydown={(e) => e.key === "Escape" && onclose()}
|
||||
transition:fade={{ duration: 200 }}
|
||||
role="button"
|
||||
tabindex="0"
|
||||
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"
|
||||
onclick={(e) => e.stopPropagation()}
|
||||
onkeydown={(e) => e.stopPropagation()}
|
||||
transition:scale={{ duration: 300, start: 0.95 }}
|
||||
role="document"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="px-6 py-5 bg-slate-800 border-b border-slate-700 flex justify-between items-center shrink-0"
|
||||
>
|
||||
<div class="modal-title-group">
|
||||
<h2 class="m-0 text-white text-xl font-bold">
|
||||
{report.company_name}
|
||||
</h2>
|
||||
<span class="block mt-1 text-slate-400 text-sm"
|
||||
>{report.title || report.filename}</span
|
||||
>
|
||||
</div>
|
||||
<button
|
||||
class="bg-transparent border-none text-slate-400 cursor-pointer p-1 flex transition-colors duration-200 hover:text-white"
|
||||
onclick={onclose}
|
||||
>
|
||||
<Icon icon="ri:close-line" width="24" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 relative overflow-hidden bg-black flex flex-col">
|
||||
{#if report.filename
|
||||
.toLowerCase()
|
||||
.endsWith(".pdf") || report.filename
|
||||
.toLowerCase()
|
||||
.endsWith(".txt")}
|
||||
<iframe
|
||||
src="http://localhost:5000/api/reports/view/{report.filename}"
|
||||
class="w-full h-full border-none"
|
||||
title="Report Viewer"
|
||||
></iframe>
|
||||
{:else}
|
||||
<div
|
||||
class="flex-1 flex flex-col items-center justify-center text-slate-400 gap-5"
|
||||
>
|
||||
<Icon
|
||||
icon="ri:file-warning-line"
|
||||
width="64"
|
||||
class="text-slate-500"
|
||||
/>
|
||||
<p>Preview not available for this file type.</p>
|
||||
<a
|
||||
href="http://localhost:5000/api/reports/view/{report.filename}"
|
||||
download
|
||||
class="bg-emerald-400 text-slate-900 px-6 py-3 rounded-full no-underline font-bold transition-transform hover:scale-105"
|
||||
>
|
||||
Download File
|
||||
</a>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
86
frontend/src/lib/components/catalogue/IncidentCard.svelte
Normal file
86
frontend/src/lib/components/catalogue/IncidentCard.svelte
Normal file
@@ -0,0 +1,86 @@
|
||||
<script lang="ts">
|
||||
import Icon from "@iconify/svelte";
|
||||
|
||||
interface Incident {
|
||||
_id: string;
|
||||
product_name: string;
|
||||
detected_brand: string;
|
||||
user_description?: string;
|
||||
created_at: string;
|
||||
analysis: {
|
||||
verdict: string;
|
||||
confidence: string;
|
||||
severity: string;
|
||||
reasoning: string;
|
||||
is_greenwashing: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
let { incident, onclick }: { incident: Incident; onclick: () => void } =
|
||||
$props();
|
||||
</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"
|
||||
{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>
|
||||
|
||||
<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">
|
||||
{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"
|
||||
>{incident.detected_brand}</span
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<p class="text-white/70 text-sm m-0 mb-2.5 leading-tight italic">
|
||||
{incident.analysis?.verdict || "Greenwashing detected"}
|
||||
</p>
|
||||
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<span
|
||||
class="flex items-center gap-1 px-2.5 py-1 rounded-full text-[11px] font-semibold capitalize
|
||||
{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" />
|
||||
{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"
|
||||
>
|
||||
<Icon icon="ri:shield-check-fill" width="14" />
|
||||
{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"
|
||||
>
|
||||
<Icon icon="ri:calendar-line" width="14" />
|
||||
{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"
|
||||
>
|
||||
<Icon icon="ri:spam-2-fill" width="24" class="text-red-500" />
|
||||
<span>Confirmed</span>
|
||||
</div>
|
||||
</button>
|
||||
233
frontend/src/lib/components/catalogue/IncidentModal.svelte
Normal file
233
frontend/src/lib/components/catalogue/IncidentModal.svelte
Normal file
@@ -0,0 +1,233 @@
|
||||
<script lang="ts">
|
||||
import Icon from "@iconify/svelte";
|
||||
import { fade, scale } from "svelte/transition";
|
||||
|
||||
interface Incident {
|
||||
_id: string;
|
||||
product_name: string;
|
||||
detected_brand: string;
|
||||
user_description?: string;
|
||||
created_at: string;
|
||||
analysis: {
|
||||
verdict: string;
|
||||
confidence: string;
|
||||
severity: string;
|
||||
reasoning: string;
|
||||
is_greenwashing: boolean;
|
||||
key_claims?: string[];
|
||||
red_flags?: string[];
|
||||
recommendations?: string;
|
||||
};
|
||||
}
|
||||
|
||||
let { incident, onclose }: { incident: Incident; onclose: () => void } =
|
||||
$props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="fixed inset-0 bg-black/60 backdrop-blur-md z-1000 flex justify-center items-center p-5 outline-none"
|
||||
onclick={onclose}
|
||||
onkeydown={(e) => e.key === "Escape" && onclose()}
|
||||
transition:fade={{ duration: 200 }}
|
||||
role="button"
|
||||
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"
|
||||
onclick={(e) => e.stopPropagation()}
|
||||
onkeydown={(e) => e.stopPropagation()}
|
||||
transition:scale={{ duration: 300, start: 0.95 }}
|
||||
role="document"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="px-10 py-[30px] bg-white/3 border-b border-red-500/20 flex justify-between items-center shrink-0"
|
||||
>
|
||||
<div class="flex flex-col">
|
||||
<div class="flex items-center gap-3">
|
||||
<Icon
|
||||
icon="ri:alert-fill"
|
||||
width="28"
|
||||
class="text-red-500"
|
||||
/>
|
||||
<h2 class="m-0 text-white text-[28px] 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"
|
||||
>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"
|
||||
onclick={onclose}
|
||||
>
|
||||
<Icon icon="ri:close-line" width="24" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="p-10 flex flex-col gap-[30px]">
|
||||
<!-- Status Badges -->
|
||||
<div class="flex flex-wrap gap-3">
|
||||
<span
|
||||
class="flex items-center gap-2 px-5 py-3 rounded-[14px] text-[11px] font-extrabold tracking-wider
|
||||
{incident.analysis?.severity === 'high'
|
||||
? 'bg-red-500/20 text-red-400 border border-red-500/30'
|
||||
: 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"
|
||||
>
|
||||
<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"
|
||||
>
|
||||
<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"
|
||||
>
|
||||
<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">
|
||||
<h3
|
||||
class="flex items-center gap-[10px] text-white text-base font-bold mb-4"
|
||||
>
|
||||
<Icon icon="ri:scales-3-fill" width="20" />
|
||||
Verdict
|
||||
</h3>
|
||||
<p
|
||||
class="text-amber-400 text-[18px] 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">
|
||||
<h3
|
||||
class="flex items-center gap-[10px] text-white text-base font-bold mb-4"
|
||||
>
|
||||
<Icon icon="ri:file-text-fill" width="20" />
|
||||
Detailed Analysis
|
||||
</h3>
|
||||
<p class="text-white/85 text-[15px] 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"
|
||||
>
|
||||
<h3
|
||||
class="flex items-center gap-[10px] text-red-400 text-base font-bold mb-4"
|
||||
>
|
||||
<Icon icon="ri:flag-fill" width="20" />
|
||||
Red Flags Identified
|
||||
</h3>
|
||||
<ul class="list-none p-0 m-0 flex flex-col gap-3">
|
||||
{#each incident.analysis.red_flags as flag}
|
||||
<li
|
||||
class="flex items-start gap-3 text-red-300/80 text-sm leading-[1.6]"
|
||||
>
|
||||
<Icon
|
||||
icon="ri:error-warning-line"
|
||||
width="16"
|
||||
class="mt-0.5 shrink-0"
|
||||
/>
|
||||
{flag}
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</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"
|
||||
>
|
||||
<h3
|
||||
class="flex items-center gap-[10px] text-white text-base font-bold mb-4"
|
||||
>
|
||||
<Icon icon="ri:chat-quote-fill" width="20" />
|
||||
Environmental Claims Made
|
||||
</h3>
|
||||
<ul class="list-none p-0 m-0 flex flex-col gap-3">
|
||||
{#each incident.analysis.key_claims as claim}
|
||||
<li
|
||||
class="flex items-start gap-3 text-white/70 text-sm italic leading-[1.6]"
|
||||
>
|
||||
<Icon
|
||||
icon="ri:double-quotes-l"
|
||||
width="16"
|
||||
class="mt-0.5 shrink-0"
|
||||
/>
|
||||
{claim}
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Recommendations -->
|
||||
{#if incident.analysis?.recommendations}
|
||||
<div
|
||||
class="bg-emerald-500/8 border border-emerald-500/20 rounded-[20px] p-6"
|
||||
>
|
||||
<h3
|
||||
class="flex items-center gap-[10px] text-emerald-400 text-base font-bold mb-4"
|
||||
>
|
||||
<Icon icon="ri:lightbulb-fill" width="20" />
|
||||
Consumer Recommendations
|
||||
</h3>
|
||||
<p class="text-emerald-300 text-[15px] 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"
|
||||
>
|
||||
<h3
|
||||
class="flex items-center gap-[10px] text-indigo-400 text-base font-bold mb-4"
|
||||
>
|
||||
<Icon icon="ri:user-voice-fill" width="20" />
|
||||
Original User Report
|
||||
</h3>
|
||||
<p
|
||||
class="text-indigo-200 text-[15px] italic leading-[1.6] m-0"
|
||||
>
|
||||
"{incident.user_description}"
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
/* Custom utility to hide scrollbar if tailwind plugin not present */
|
||||
.scrollbar-hide {
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
}
|
||||
.scrollbar-hide::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
53
frontend/src/lib/components/catalogue/Pagination.svelte
Normal file
53
frontend/src/lib/components/catalogue/Pagination.svelte
Normal file
@@ -0,0 +1,53 @@
|
||||
<script lang="ts">
|
||||
import Icon from "@iconify/svelte";
|
||||
|
||||
let {
|
||||
currentPage = $bindable(),
|
||||
totalPages,
|
||||
goToPage,
|
||||
}: {
|
||||
currentPage: number;
|
||||
totalPages: number;
|
||||
goToPage: (p: number) => void;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
{#if totalPages > 1}
|
||||
<div class="flex items-center justify-center gap-4 mt-6">
|
||||
<button
|
||||
class="w-12 h-12 flex items-center justify-center rounded-xl bg-white/5 border border-white/10 text-white cursor-pointer transition-all duration-200 hover:enabled:bg-white/15 hover:enabled:scale-105 disabled:opacity-30 disabled:cursor-not-allowed"
|
||||
disabled={currentPage === 1}
|
||||
onclick={() => goToPage(currentPage - 1)}
|
||||
>
|
||||
<Icon icon="ri:arrow-left-s-line" width="24" />
|
||||
</button>
|
||||
|
||||
<div class="flex gap-2">
|
||||
{#if totalPages <= 7}
|
||||
{#each Array(totalPages) as _, i}
|
||||
<button
|
||||
class="px-[18px] h-12 flex items-center justify-center rounded-xl border font-bold text-sm cursor-pointer transition-all duration-200 hover:enabled:bg-white/15 hover:enabled:scale-105 disabled:opacity-30 disabled:cursor-not-allowed
|
||||
{currentPage === i + 1
|
||||
? 'bg-emerald-500 border-emerald-500 text-emerald-950'
|
||||
: 'bg-white/5 border-white/10 text-white'}"
|
||||
onclick={() => goToPage(i + 1)}
|
||||
>
|
||||
{i + 1}
|
||||
</button>
|
||||
{/each}
|
||||
{:else}
|
||||
<span class="text-white/50 text-sm font-semibold self-center">
|
||||
Page {currentPage} of {totalPages}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="w-12 h-12 flex items-center justify-center rounded-xl bg-white/5 border border-white/10 text-white cursor-pointer transition-all duration-200 hover:enabled:bg-white/15 hover:enabled:scale-105 disabled:opacity-30 disabled:cursor-not-allowed"
|
||||
disabled={currentPage === totalPages}
|
||||
onclick={() => goToPage(currentPage + 1)}
|
||||
>
|
||||
<Icon icon="ri:arrow-right-s-line" width="24" />
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
120
frontend/src/lib/components/catalogue/ReportCard.svelte
Normal file
120
frontend/src/lib/components/catalogue/ReportCard.svelte
Normal file
@@ -0,0 +1,120 @@
|
||||
<script lang="ts">
|
||||
import Icon from "@iconify/svelte";
|
||||
|
||||
interface Report {
|
||||
company_name: string;
|
||||
year: string | number;
|
||||
sector: string;
|
||||
greenwashing_score: number | string;
|
||||
filename: string;
|
||||
title?: string;
|
||||
snippet?: string;
|
||||
}
|
||||
|
||||
let {
|
||||
report,
|
||||
openReport,
|
||||
searchQuery,
|
||||
}: {
|
||||
report: Report;
|
||||
openReport: (r: Report) => void;
|
||||
searchQuery: string;
|
||||
} = $props();
|
||||
|
||||
function getFileDetails(filename: string) {
|
||||
const ext = filename.split(".").pop()?.toLowerCase();
|
||||
if (ext === "pdf")
|
||||
return { type: "PDF Document", icon: "ri:file-pdf-2-line" };
|
||||
if (ext === "xlsx" || ext === "csv")
|
||||
return { type: "Data Spreadsheet", icon: "ri:file-excel-2-line" };
|
||||
if (ext === "txt")
|
||||
return { type: "Text Report", icon: "ri:file-text-line" };
|
||||
return { type: "Impact Report", icon: "ri:file-info-line" };
|
||||
}
|
||||
|
||||
function getScoreColor(score: string | number) {
|
||||
const s = Number(score);
|
||||
if (s >= 80) return "bg-emerald-500";
|
||||
if (s >= 50) return "bg-amber-500";
|
||||
return "bg-red-500";
|
||||
}
|
||||
|
||||
const fileDetails = $derived(getFileDetails(report.filename));
|
||||
</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"
|
||||
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"
|
||||
>
|
||||
<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">
|
||||
{report.company_name}
|
||||
</h3>
|
||||
<span
|
||||
class="text-emerald-400 bg-emerald-500/10 px-2 py-0.5 rounded-md text-[12px] 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"
|
||||
>
|
||||
{@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>`,
|
||||
)}
|
||||
</p>
|
||||
{:else}
|
||||
<p class="text-white/40 text-sm mb-4 m-0">
|
||||
{report.sector} Sector • Impact Report
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<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"
|
||||
title={report.filename}
|
||||
>
|
||||
<Icon icon={fileDetails.icon} width="14" />
|
||||
{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"
|
||||
>
|
||||
<Icon
|
||||
icon="ri:checkbox-circle-fill"
|
||||
width="14"
|
||||
class="text-emerald-500"
|
||||
/>
|
||||
Analyzed
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if report.greenwashing_score}
|
||||
<div class="text-center ml-5">
|
||||
<div
|
||||
class="w-[52px] h-[52px] rounded-xe flex items-center justify-center mb-1 rounded-[14px] {getScoreColor(
|
||||
report.greenwashing_score,
|
||||
)}"
|
||||
>
|
||||
<span class="text-emerald-950 text-[18px] font-black"
|
||||
>{Math.round(Number(report.greenwashing_score))}</span
|
||||
>
|
||||
</div>
|
||||
<span
|
||||
class="text-white/40 text-[10px] font-extrabold uppercase tracking-widest"
|
||||
>Trust Score</span
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
</button>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -5,6 +5,9 @@
|
||||
let description = $state("");
|
||||
let image = $state<string | null>(null);
|
||||
let submitted = $state(false);
|
||||
let isLoading = $state(false);
|
||||
let analysisResult = $state<any>(null);
|
||||
let error = $state<string | null>(null);
|
||||
|
||||
$effect(() => {
|
||||
const initialName = new URLSearchParams(window.location.search).get(
|
||||
@@ -37,12 +40,75 @@
|
||||
input.click();
|
||||
}
|
||||
|
||||
function handleSubmit() {
|
||||
if (!isValid) return;
|
||||
submitted = true;
|
||||
setTimeout(() => {
|
||||
window.history.back();
|
||||
}, 2000);
|
||||
const progressSteps = [
|
||||
{ id: 1, label: "Scanning image...", icon: "ri:camera-lens-line" },
|
||||
{
|
||||
id: 2,
|
||||
label: "Detecting brand logos...",
|
||||
icon: "ri:search-eye-line",
|
||||
},
|
||||
{ id: 3, label: "Searching database...", icon: "ri:database-2-line" },
|
||||
{ id: 4, label: "AI analysis in progress...", icon: "ri:robot-2-line" },
|
||||
{
|
||||
id: 5,
|
||||
label: "Generating verdict...",
|
||||
icon: "ri:file-shield-2-line",
|
||||
},
|
||||
];
|
||||
|
||||
let currentStep = $state(0);
|
||||
|
||||
async function handleSubmit() {
|
||||
if (!isValid || isLoading) return;
|
||||
|
||||
isLoading = true;
|
||||
error = null;
|
||||
currentStep = 1;
|
||||
|
||||
// Simulate progress steps
|
||||
const stepInterval = setInterval(() => {
|
||||
if (currentStep < progressSteps.length) {
|
||||
currentStep++;
|
||||
}
|
||||
}, 1500);
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
"http://localhost:5000/api/incidents/submit",
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
product_name: productName,
|
||||
description: description,
|
||||
image: image, // Base64 encoded
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
clearInterval(stepInterval);
|
||||
currentStep = progressSteps.length; // Complete all steps
|
||||
|
||||
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;
|
||||
} else {
|
||||
error = data.message || "Failed to submit report";
|
||||
}
|
||||
} catch (e) {
|
||||
clearInterval(stepInterval);
|
||||
error = "Failed to connect to server. Please try again.";
|
||||
console.error("Submit error:", e);
|
||||
} finally {
|
||||
isLoading = false;
|
||||
currentStep = 0;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -52,19 +118,79 @@
|
||||
</div>
|
||||
|
||||
<div class="content-container">
|
||||
{#if submitted}
|
||||
{#if submitted && analysisResult}
|
||||
<div class="glass-card success-card">
|
||||
<div class="icon-circle success">
|
||||
<iconify-icon
|
||||
icon="ri:checkbox-circle-fill"
|
||||
width="60"
|
||||
style="color: #4ade80;"
|
||||
></iconify-icon>
|
||||
{#if analysisResult.is_greenwashing}
|
||||
<div class="icon-circle warning">
|
||||
<iconify-icon
|
||||
icon="ri:alert-fill"
|
||||
width="60"
|
||||
style="color: #f59e0b;"
|
||||
></iconify-icon>
|
||||
</div>
|
||||
<h2 class="success-title warning-text">
|
||||
Greenwashing Detected!
|
||||
</h2>
|
||||
{:else}
|
||||
<div class="icon-circle success">
|
||||
<iconify-icon
|
||||
icon="ri:checkbox-circle-fill"
|
||||
width="60"
|
||||
style="color: #4ade80;"
|
||||
></iconify-icon>
|
||||
</div>
|
||||
<h2 class="success-title">Report Analyzed</h2>
|
||||
{/if}
|
||||
|
||||
<div class="analysis-result">
|
||||
<div class="result-item">
|
||||
<span class="result-label">Verdict:</span>
|
||||
<span class="result-value"
|
||||
>{analysisResult.analysis?.verdict || "N/A"}</span
|
||||
>
|
||||
</div>
|
||||
<div class="result-item">
|
||||
<span class="result-label">Confidence:</span>
|
||||
<span
|
||||
class="result-value badge {analysisResult.analysis
|
||||
?.confidence}"
|
||||
>
|
||||
{analysisResult.analysis?.confidence || "unknown"}
|
||||
</span>
|
||||
</div>
|
||||
{#if analysisResult.is_greenwashing}
|
||||
<div class="result-item">
|
||||
<span class="result-label">Severity:</span>
|
||||
<span
|
||||
class="result-value badge severity-{analysisResult
|
||||
.analysis?.severity}"
|
||||
>
|
||||
{analysisResult.analysis?.severity || "unknown"}
|
||||
</span>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="result-item full-width">
|
||||
<span class="result-label">Analysis:</span>
|
||||
<p class="result-text">
|
||||
{analysisResult.analysis?.reasoning ||
|
||||
"No details available"}
|
||||
</p>
|
||||
</div>
|
||||
{#if analysisResult.detected_brand && analysisResult.detected_brand !== "Unknown"}
|
||||
<div class="result-item">
|
||||
<span class="result-label">Detected Brand:</span>
|
||||
<span class="result-value"
|
||||
>{analysisResult.detected_brand}</span
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<h2 class="success-title">Report Submitted!</h2>
|
||||
<p class="success-subtitle">
|
||||
Thank you for keeping companies honest.
|
||||
</p>
|
||||
|
||||
<button class="back-btn" onclick={() => window.history.back()}>
|
||||
<iconify-icon icon="ri:arrow-left-line" width="20"
|
||||
></iconify-icon>
|
||||
Go Back
|
||||
</button>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="header-section">
|
||||
@@ -143,17 +269,80 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="submit-button"
|
||||
class:disabled={!isValid}
|
||||
disabled={!isValid}
|
||||
onclick={handleSubmit}
|
||||
type="button"
|
||||
>
|
||||
<iconify-icon icon="ri:alert-fill" width="20"
|
||||
></iconify-icon>
|
||||
Submit Report
|
||||
</button>
|
||||
{#if error}
|
||||
<div class="error-message">
|
||||
<iconify-icon
|
||||
icon="ri:error-warning-line"
|
||||
width="18"
|
||||
></iconify-icon>
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if isLoading}
|
||||
<div class="progress-container">
|
||||
<div class="progress-header">
|
||||
<iconify-icon
|
||||
icon="ri:shield-check-line"
|
||||
width="24"
|
||||
class="pulse"
|
||||
></iconify-icon>
|
||||
<span>Analyzing Report</span>
|
||||
</div>
|
||||
<div class="progress-steps">
|
||||
{#each progressSteps as step}
|
||||
<div
|
||||
class="progress-step"
|
||||
class:active={currentStep >= step.id}
|
||||
class:current={currentStep === step.id}
|
||||
>
|
||||
<div class="step-icon">
|
||||
{#if currentStep > step.id}
|
||||
<iconify-icon
|
||||
icon="ri:check-line"
|
||||
width="16"
|
||||
></iconify-icon>
|
||||
{:else if currentStep === step.id}
|
||||
<iconify-icon
|
||||
icon={step.icon}
|
||||
width="16"
|
||||
class="spin-slow"
|
||||
></iconify-icon>
|
||||
{:else}
|
||||
<iconify-icon
|
||||
icon={step.icon}
|
||||
width="16"
|
||||
></iconify-icon>
|
||||
{/if}
|
||||
</div>
|
||||
<span class="step-label"
|
||||
>{step.label}</span
|
||||
>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="progress-bar">
|
||||
<div
|
||||
class="progress-fill"
|
||||
style="width: {(currentStep /
|
||||
progressSteps.length) *
|
||||
100}%"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<button
|
||||
class="submit-button"
|
||||
class:disabled={!isValid}
|
||||
disabled={!isValid}
|
||||
onclick={handleSubmit}
|
||||
type="button"
|
||||
>
|
||||
<iconify-icon icon="ri:shield-flash-line" width="20"
|
||||
></iconify-icon>
|
||||
Analyze for Greenwashing
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -175,22 +364,22 @@
|
||||
.content-container {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
padding: 100px 24px 120px;
|
||||
max-width: 600px;
|
||||
padding: 80px 20px 40px;
|
||||
max-width: 500px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.header-section {
|
||||
text-align: center;
|
||||
margin-bottom: 32px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
color: white;
|
||||
font-size: 36px;
|
||||
font-size: 28px;
|
||||
font-weight: 900;
|
||||
margin: 0;
|
||||
letter-spacing: -1px;
|
||||
letter-spacing: -0.5px;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
@@ -212,19 +401,19 @@
|
||||
}
|
||||
|
||||
.form-card {
|
||||
padding: 36px;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.form-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.label {
|
||||
@@ -412,6 +601,239 @@
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.warning-text {
|
||||
color: #f59e0b !important;
|
||||
}
|
||||
|
||||
.icon-circle.warning {
|
||||
background: rgba(245, 158, 11, 0.15);
|
||||
}
|
||||
|
||||
.analysis-result {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
margin-top: 20px;
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.result-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.result-item.full-width {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.result-label {
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
.result-value {
|
||||
color: white;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.result-text {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
margin: 4px 0 0 0;
|
||||
}
|
||||
|
||||
.badge {
|
||||
padding: 2px 10px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.badge.high {
|
||||
background: #ef4444;
|
||||
color: white;
|
||||
}
|
||||
.badge.medium {
|
||||
background: #f59e0b;
|
||||
color: #000;
|
||||
}
|
||||
.badge.low {
|
||||
background: #22c55e;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.severity-high {
|
||||
background: #ef4444 !important;
|
||||
}
|
||||
.severity-medium {
|
||||
background: #f59e0b !important;
|
||||
}
|
||||
.severity-low {
|
||||
background: #22c55e !important;
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
margin-top: 24px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
color: white;
|
||||
padding: 12px 24px;
|
||||
border-radius: 50px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.back-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.error-message {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 12px 16px;
|
||||
background: rgba(239, 68, 68, 0.15);
|
||||
border: 1px solid rgba(239, 68, 68, 0.3);
|
||||
border-radius: 12px;
|
||||
color: #fca5a5;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
:global(.spin) {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
:global(.spin-slow) {
|
||||
animation: spin 2s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
opacity: 0.7;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
}
|
||||
|
||||
:global(.pulse) {
|
||||
animation: pulse 1.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.progress-container {
|
||||
background: rgba(34, 197, 94, 0.08);
|
||||
border: 1px solid rgba(34, 197, 94, 0.2);
|
||||
border-radius: 16px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.progress-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
color: #4ade80;
|
||||
font-weight: 700;
|
||||
font-size: 16px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.progress-steps {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.progress-step {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
opacity: 0.4;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.progress-step.active {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.progress-step.current {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.progress-step.current .step-label {
|
||||
color: #4ade80;
|
||||
}
|
||||
|
||||
.step-icon {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.progress-step.active .step-icon {
|
||||
background: rgba(34, 197, 94, 0.2);
|
||||
color: #4ade80;
|
||||
}
|
||||
|
||||
.progress-step.current .step-icon {
|
||||
background: #4ade80;
|
||||
color: #051f18;
|
||||
}
|
||||
|
||||
.step-label {
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.progress-step.active .step-label {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
height: 4px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 2px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #22c55e, #4ade80);
|
||||
border-radius: 2px;
|
||||
transition: width 0.5s ease;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.desktop-bg {
|
||||
display: block;
|
||||
|
||||
Reference in New Issue
Block a user