diff --git a/apps/web-ui/src/app/components/EvaluateForm.tsx b/apps/web-ui/src/app/components/EvaluateForm.tsx
new file mode 100644
index 0000000..1b4c959
--- /dev/null
+++ b/apps/web-ui/src/app/components/EvaluateForm.tsx
@@ -0,0 +1,99 @@
+"use client";
+
+import { useState } from "react";
+import { useRouter } from "next/navigation";
+import { evaluate } from "@cv-builder/core";
+import { TextStats } from "./TextStats";
+import { saveEvaluationResult } from "../lib/evaluation-storage";
+import { FileUpload } from "./FileUpload";
+
+export function EvaluateForm() {
+ const router = useRouter();
+
+ const [cv, setCv] = useState("");
+ const [jd, setJd] = useState("");
+ const [loading, setLoading] = useState(false);
+
+ async function handleEvaluate() {
+ if (!cv.trim()) {
+ alert("Please paste your CV.");
+ return;
+ }
+
+
+ setLoading(true);
+
+ try {
+ const result = await evaluate({
+ cv: {
+ content: cv,
+ format: "plaintext",
+ },
+ jd: {
+ content: jd,
+ },
+ });
+
+ saveEvaluationResult(result);
+
+ router.push("/results");
+ } catch (error) {
+ console.error(error);
+ alert("Evaluation failed.");
+ } finally {
+ setLoading(false);
+ }
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/apps/web-ui/src/app/components/FileUpload.tsx b/apps/web-ui/src/app/components/FileUpload.tsx
new file mode 100644
index 0000000..b9c7fb9
--- /dev/null
+++ b/apps/web-ui/src/app/components/FileUpload.tsx
@@ -0,0 +1,87 @@
+"use client";
+
+import { DragEvent, useRef, useState } from "react";
+
+interface FileUploadProps {
+ onContentLoaded: (content: string) => void;
+}
+
+export function FileUpload({
+ onContentLoaded,
+}: FileUploadProps) {
+ const [fileName, setFileName] = useState("");
+ const inputRef = useRef(null);
+
+ async function processFile(file: File) {
+ const extension = file.name.split(".").pop()?.toLowerCase() || "";
+
+ if (extension === "txt" || extension === "md") {
+ try {
+ const text = await file.text();
+ onContentLoaded(text);
+ setFileName(file.name);
+ } catch (error) {
+ console.error("Failed to read file:", error);
+ alert("Failed to read file. Please try again.");
+ setFileName("");
+ }
+ return;
+ }
+
+ if (extension === "pdf") {
+ alert(
+ "PDF text extraction is not yet supported. Please use .txt or .md files."
+ );
+ return;
+ }
+
+ alert("Unsupported file type.");
+ }
+
+ async function handleFiles(files: FileList | null) {
+ if (!files?.length) return;
+
+ await processFile(files[0]);
+ }
+
+ async function handleDrop(
+ event: DragEvent
+ ) {
+ event.preventDefault();
+
+ await handleFiles(event.dataTransfer.files);
+ }
+
+ return (
+ <>
+ e.preventDefault()}
+ onDrop={handleDrop}
+ onClick={() => inputRef.current?.click()}
+ className="mt-4 cursor-pointer rounded-xl border-2 border-dashed border-zinc-300 p-6 text-center transition hover:border-zinc-500"
+ >
+
+ Drag & Drop a file here
+
+
+
+ Supports .txt and .md
+
+
+ {fileName && (
+
+ Loaded: {fileName}
+
+ )}
+
+
+ handleFiles(e.target.files)}
+ />
+ >
+ );
+}
\ No newline at end of file
diff --git a/apps/web-ui/src/app/components/ScoreCard.tsx b/apps/web-ui/src/app/components/ScoreCard.tsx
new file mode 100644
index 0000000..40a3c66
--- /dev/null
+++ b/apps/web-ui/src/app/components/ScoreCard.tsx
@@ -0,0 +1,37 @@
+interface ScoreCardProps {
+ score: number;
+}
+
+export function ScoreCard({
+ score,
+}: ScoreCardProps) {
+ if (typeof score !== 'number' || !isFinite(score) || score < 0) {
+ console.warn('ScoreCard received invalid score:', score);
+ score = 0;
+ }
+ const percentage = Math.min(
+ 100,
+ (score / 5) * 100
+ );
+
+ return (
+
+
+ Overall Score
+
+
+
+ {score}
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/apps/web-ui/src/app/components/TextStats.tsx b/apps/web-ui/src/app/components/TextStats.tsx
new file mode 100644
index 0000000..694bf46
--- /dev/null
+++ b/apps/web-ui/src/app/components/TextStats.tsx
@@ -0,0 +1,16 @@
+interface TextStatsProps {
+ value: string;
+}
+
+export function TextStats({ value }: TextStatsProps) {
+ const trimmed = value.trim();
+ const words = trimmed ? trimmed.split(/\s+/).length : 0;
+
+ const characters = value.length;
+
+ return (
+
+ {characters} chars • {words} words
+
+ );
+}
\ No newline at end of file
diff --git a/apps/web-ui/src/app/lib/evaluation-storage.ts b/apps/web-ui/src/app/lib/evaluation-storage.ts
new file mode 100644
index 0000000..3aaceb4
--- /dev/null
+++ b/apps/web-ui/src/app/lib/evaluation-storage.ts
@@ -0,0 +1,41 @@
+export const EVALUATION_RESULT_KEY =
+ "evaluation-result";
+
+export function saveEvaluationResult(
+ data: unknown
+) {
+ if (typeof window === "undefined") {
+ return;
+ }
+
+ try {
+ localStorage.setItem(
+ EVALUATION_RESULT_KEY,
+ JSON.stringify(data)
+ );
+ } catch (error) {
+ console.error("Failed to save evaluation result:", error);
+ // Optionally notify the user
+ }
+}
+
+export function getEvaluationResult() {
+ if (typeof window === "undefined") {
+ return null;
+ }
+
+ const raw = localStorage.getItem(
+ EVALUATION_RESULT_KEY
+ );
+
+ if (!raw) {
+ return null;
+ }
+
+ try {
+ return JSON.parse(raw);
+ } catch (error) {
+ console.error("Failed to parse evaluation result:", error);
+ return null;
+ }
+}
\ No newline at end of file
diff --git a/apps/web-ui/src/app/page.tsx b/apps/web-ui/src/app/page.tsx
index 95e9f85..855f404 100644
--- a/apps/web-ui/src/app/page.tsx
+++ b/apps/web-ui/src/app/page.tsx
@@ -1,4 +1,5 @@
import { Header } from "./components/layout/Header";
+import { EvaluateForm } from "./components/EvaluateForm";
export default function Home() {
return (
@@ -46,6 +47,8 @@ export default function Home() {
+
+
diff --git a/apps/web-ui/src/app/results/page.tsx b/apps/web-ui/src/app/results/page.tsx
new file mode 100644
index 0000000..19bf483
--- /dev/null
+++ b/apps/web-ui/src/app/results/page.tsx
@@ -0,0 +1,86 @@
+"use client";
+
+import { useEffect, useState } from "react";
+import { getEvaluationResult } from "../lib/evaluation-storage";
+import { ScoreCard } from "../components/ScoreCard";
+
+export default function ResultsPage() {
+ const [result, setResult] = useState(null);
+ const [loading, setLoading] = useState(true);
+
+ useEffect(() => {
+ const data = getEvaluationResult();
+
+ setResult(data);
+ setLoading(false);
+ }, []);
+
+ if (loading) {
+ return (
+
+ Loading...
+
+ );
+ }
+
+ if (!result) {
+ return (
+
+ No evaluation found
+
+ );
+ }
+
+ return (
+
+
+ Evaluation Results
+
+
+
+
+
+
+ Strengths
+
+
+
+ {result.strengths.map(
+ (strength: string, index: number) => (
+ -
+ • {strength}
+
+ )
+ )}
+
+
+
+
+
+ Dimensions
+
+
+ {result.dimensions.map(
+ (dimension: any, index: number) => (
+
+
+
+ {dimension.name}
+
+
+
+ {dimension.score}
+ /
+ {dimension.maxScore}
+
+
+
+ )
+ )}
+
+
+ );
+}
\ No newline at end of file